From 4ab7d0a4fa5fe07fb6a235710f0cf510b7ba71f0 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 24 Jun 2012 19:35:27 +0200 Subject: [PATCH 001/648] Modifications to random walker segmentation algorithm: * returning the probability to belong to a label instead of only the most likely label is now possible * fixing some type issues * handling non-consecutive label values --- .../random_walker_segmentation.py | 83 ++++++++++++++----- .../segmentation/tests/test_random_walker.py | 33 ++++++++ 2 files changed, 93 insertions(+), 23 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 130be926..a5bed48b 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -27,6 +27,8 @@ try: except ImportError: amg_loaded = False from scipy.sparse.linalg import cg +from ..util import img_as_float +from ..filter import rank_order #-----------Laplacian-------------------- @@ -96,7 +98,10 @@ def _make_laplacian_sparse(edges, weights): return lap.tocsr() -def _clean_labels_ar(X, labels): +def _clean_labels_ar(X, labels, copy=False): + X = X.astype(labels.dtype) + if copy: + labels = np.copy(labels) labels = np.ravel(labels) labels[labels == 0] = X return labels @@ -157,7 +162,8 @@ def _build_laplacian(data, mask=None, beta=50): #----------- Random walker algorithm -------------------------------- -def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): +def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, + return_full_prob=False, reorder_labels=False): """ Random walker algorithm for segmentation from markers. @@ -172,7 +178,9 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): Array of seed markers labeled with different positive integers for different phases. Zero-labeled pixels are unlabeled pixels. Negative labels correspond to inactive pixels that are not taken - into account (they are removed from the graph). + into account (they are removed from the graph). If labels are not + consecutive integers and `reorder_labels` is True, the labels array + will be transformed so that labels are consecutive. beta : float Penalization coefficient for the random walker motion @@ -208,12 +216,24 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): the result of the segmentation. Use copy=False if you want to save on memory. + return_full_prob : bool, default False + If True, the probability that a pixel belongs to each of the labels + will be returned, instead of only the most likely label. + + reorder_labels : bool, default False + If True, labels is transformed so that its values are consecutive + integers. + Returns ------- - output : ndarray of ints - Array in which each pixel has been labeled according to the marker - that reached the pixel first by anisotropic diffusion. + output : ndarray + If `return_full_prob` is False, array of ints of same shape as `data`, + in which each pixel has been labeled according to the marker that + reached the pixel first by anisotropic diffusion. + If `return_full_prob` is True, array of floats of shape + `(nlabels, data.shape)`. `output[label_nb, i, j]` is the probability + that label `label_nb` reaches the pixel `(i, j)` first. See also -------- @@ -247,7 +267,8 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): The weight w_ij is a decreasing function of the norm of the local gradient. This ensures that diffusion is easier between pixels of similar values. - When the Laplacian is decomposed into blocks of marked and unmarked pixels:: + When the Laplacian is decomposed into blocks of marked and unmarked + pixels:: L = M B.T B A @@ -257,7 +278,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): A x = - B x_m - where x_m=1 on markers of the given phase, and 0 on other markers. + where x_m = 1 on markers of the given phase, and 0 on other markers. This linear system is solved in the algorithm using a direct method for small images, and an iterative method for larger images. @@ -282,11 +303,15 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]]) """ - # We work with 3-D arrays + # We work with 3-D arrays of floats + data = img_as_float(data) data = np.atleast_3d(data) if copy: labels = np.copy(labels) - labels = labels.astype(np.intp) + if reorder_labels: + mask = labels >= 0 + labels[mask] = rank_order(labels[mask])[0].astype(labels.dtype) + labels = labels.astype(np.int32) # If the array has pruned zones, be sure that no isolated pixels # exist between pruned zones (they could not be determined) if np.any(labels < 0): @@ -304,7 +329,8 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): # where X[i, j] is the probability that a marker of label i arrives # first at pixel j by anisotropic diffusion. if mode == 'cg': - X = _solve_cg(lap_sparse, B, tol=tol) + X = _solve_cg(lap_sparse, B, tol=tol, + return_full_prob=return_full_prob) if mode == 'cg_mg': if not amg_loaded: warnings.warn( @@ -313,15 +339,23 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True): instead.""") X = _solve_cg(lap_sparse, B, tol=tol) else: - X = _solve_cg_mg(lap_sparse, B, tol=tol) + X = _solve_cg_mg(lap_sparse, B, tol=tol, + return_full_prob=return_full_prob) if mode == 'bf': - X = _solve_bf(lap_sparse, B) - X = _clean_labels_ar(X + 1, labels) + X = _solve_bf(lap_sparse, B, + return_full_prob=return_full_prob) + # Clean up results data = np.squeeze(data) - return X.reshape(data.shape) + if return_full_prob: + labels = labels.astype(np.float) + X = np.array([_clean_labels_ar(Xline, labels, + copy=True).reshape(data.shape) for Xline in X]) + else: + X = _clean_labels_ar(X + 1, labels).reshape(data.shape) + return X -def _solve_bf(lap_sparse, B): +def _solve_bf(lap_sparse, B, return_full_prob=False): """ solves lap_sparse X_i = B_i for each phase i. An LU decomposition of lap_sparse is computed first. For each pixel, the label i @@ -331,11 +365,12 @@ def _solve_bf(lap_sparse, B): solver = sparse.linalg.factorized(lap_sparse.astype(np.double)) X = np.array([solver(np.array((-B[i]).todense()).ravel())\ for i in range(len(B))]) - X = np.argmax(X, axis=0) + if not return_full_prob: + X = np.argmax(X, axis=0) return X -def _solve_cg(lap_sparse, B, tol): +def _solve_cg(lap_sparse, B, tol, return_full_prob=False): """ solves lap_sparse X_i = B_i for each phase i, using the conjugate gradient method. For each pixel, the label i corresponding to the @@ -346,12 +381,13 @@ def _solve_cg(lap_sparse, B, tol): for i in range(len(B)): x0 = cg(lap_sparse, -B[i].todense(), tol=tol)[0] X.append(x0) - X = np.array(X) - X = np.argmax(X, axis=0) + if not return_full_prob: + X = np.array(X) + X = np.argmax(X, axis=0) return X -def _solve_cg_mg(lap_sparse, B, tol): +def _solve_cg_mg(lap_sparse, B, tol, return_full_prob=False): """ solves lap_sparse X_i = B_i for each phase i, using the conjugate gradient method with a multigrid preconditioner (ruge-stuben from @@ -364,6 +400,7 @@ def _solve_cg_mg(lap_sparse, B, tol): for i in range(len(B)): x0 = cg(lap_sparse, -B[i].todense(), tol=tol, M=M, maxiter=30)[0] X.append(x0) - X = np.array(X) - X = np.argmax(X, axis=0) + if not return_full_prob: + X = np.array(X) + X = np.argmax(X, axis=0) return X diff --git a/skimage/segmentation/tests/test_random_walker.py b/skimage/segmentation/tests/test_random_walker.py index 41a86dc3..aec9edea 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -54,6 +54,10 @@ def test_2d_bf(): data, labels = make_2d_syntheticdata(lx, ly) labels_bf = random_walker(data, labels, beta=90, mode='bf') assert (labels_bf[25:45, 40:60] == 2).all() + full_prob_bf = random_walker(data, labels, beta=90, mode='bf', + return_full_prob=True) + assert (full_prob_bf[1, 25:45, 40:60] >= + full_prob_bf[0, 25:45, 40:60]).all() return data, labels_bf @@ -63,6 +67,10 @@ def test_2d_cg(): data, labels = make_2d_syntheticdata(lx, ly) labels_cg = random_walker(data, labels, beta=90, mode='cg') assert (labels_cg[25:45, 40:60] == 2).all() + full_prob = random_walker(data, labels, beta=90, mode='cg', + return_full_prob=True) + assert (full_prob[1, 25:45, 40:60] >= + full_prob[0, 25:45, 40:60]).all() return data, labels_cg @@ -72,8 +80,33 @@ def test_2d_cg_mg(): data, labels = make_2d_syntheticdata(lx, ly) labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg') assert (labels_cg_mg[25:45, 40:60] == 2).all() + full_prob = random_walker(data, labels, beta=90, mode='cg_mg', + return_full_prob=True) + assert (full_prob[1, 25:45, 40:60] >= + full_prob[0, 25:45, 40:60]).all() return data, labels_cg_mg +def test_types(): + lx = 70 + ly = 100 + data, labels = make_2d_syntheticdata(lx, ly) + data = 255 * (data - data.min()) / (data.max() - data.min()) + data = data.astype(np.uint8) + labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg') + assert (labels_cg_mg[25:45, 40:60] == 2).all() + return data, labels_cg_mg + +def test_reorder_labels(): + lx = 70 + ly = 100 + data, labels = make_2d_syntheticdata(lx, ly) + labels[labels == 2] == 4 + labels_bf = random_walker(data, labels, beta=90, mode='bf', + reorder_labels=True) + assert (labels_bf[25:45, 40:60] == 2).all() + return data, labels_bf + + def test_2d_inactive(): lx = 70 From 249fa149ef11e81d8a82af54d94a49e724cef3af Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 24 Jun 2012 21:46:12 -0700 Subject: [PATCH 002/648] PKG: Start 0.7 development cycle. --- bento.info | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bento.info b/bento.info index f2d303db..2f32afd4 100644 --- a/bento.info +++ b/bento.info @@ -1,5 +1,5 @@ Name: scikits-image -Version: 0.6 +Version: 0.7.0.dev Summary: Image processing routines for SciPy Url: http://scikits-image.org DownloadUrl: http://github.com/scikits-image/scikits-image diff --git a/setup.py b/setup.py index 63176520..efa5f8db 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ MAINTAINER_EMAIL = 'stefan@sun.ac.za' URL = 'http://scikits-image.org' LICENSE = 'Modified BSD' DOWNLOAD_URL = 'http://github.com/scikits-image/scikits-image' -VERSION = '0.6' +VERSION = '0.7dev' import os import setuptools From abb5cc10f7e8a2961abeff993cfc3c1f30b105c8 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 24 Jun 2012 21:46:29 -0700 Subject: [PATCH 003/648] PKG: Use different delimiter in contributors list. --- doc/release/contributors.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release/contributors.sh b/doc/release/contributors.sh index caf553ee..023928d3 100755 --- a/doc/release/contributors.sh +++ b/doc/release/contributors.sh @@ -1,2 +1,2 @@ -git log $1..HEAD --format='* %aN' | sed 's/@/\-at\-/' | sed 's/<>//' | sort -u +git log $1..HEAD --format='- %aN' | sed 's/@/\-at\-/' | sed 's/<>//' | sort -u From b891a7c9d18a2a40942d3cec5608597e4cb9de1c Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 24 Jun 2012 21:50:38 -0700 Subject: [PATCH 004/648] DOC: Update release instructions. --- RELEASE.txt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/RELEASE.txt b/RELEASE.txt index cea5adb7..326a5900 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -5,9 +5,12 @@ How to make a new release of ``skimage`` - Update the version number in setup.py and bento.info and commit - Update the docs: - Edit ``doc/source/themes/agogo/static/docversions.js`` and commit - - Build a clean version of the docs. Run "make" in the root dir, then + - Build a clean version of the docs: run "make" in the root dir, then ``rm build -rf; make html`` in the docs. - - Push upstream using "make gh-pages" + - Push upstream using "make gh-pages; cd gh-pages; git push" + (Note: the version list won't display correctly until after you've also + rebuilt the dev docs later on; they're grabbed from + ``skimage.org/docs/dev/.../docversions.js``.) - Add the version number as a tag in git:: git tag v0.6 @@ -21,7 +24,8 @@ How to make a new release of ``skimage`` python setup.py register python setup.py sdist upload -- Increase the version number in the setup.py file to ``0.Xdev``. +- Increase the version number in the setup.py and bento.info file to ``0.Xdev`` + and ``0.X.0.dev`` respectively. - Update the web frontpage: The webpage is kept in a separate repo: scikits-image-web @@ -29,3 +33,5 @@ How to make a new release of ``skimage`` - ``index.rst`` - Post release notes on mailing lists, blog, G+, etc. + +- Regenerate the dev docs, upload. From f7b3d8062cb2bc0768e414f1f02d9b9c0ae2d8b0 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 15 Jun 2012 20:55:57 +0200 Subject: [PATCH 005/648] COSMIT pep8 --- skimage/__init__.py | 6 +- skimage/_build.py | 9 +- skimage/color/colorconv.py | 2 +- skimage/color/tests/test_colorconv.py | 43 ++-- skimage/data/__init__.py | 7 + skimage/data/tests/test_data.py | 4 +- skimage/draw/setup.py | 13 +- skimage/draw/tests/test_draw.py | 32 ++- skimage/exposure/exposure.py | 1 - skimage/exposure/tests/test_exposure.py | 1 - skimage/feature/greycomatrix.py | 80 +++--- skimage/feature/harris.py | 11 +- skimage/feature/hog.py | 8 +- skimage/feature/peak.py | 13 +- skimage/feature/setup.py | 13 +- skimage/feature/template.py | 3 +- skimage/feature/tests/test_glcm.py | 62 ++--- skimage/feature/tests/test_harris.py | 4 +- skimage/feature/tests/test_hog.py | 13 +- skimage/feature/tests/test_peak.py | 1 - skimage/feature/tests/test_template.py | 5 +- skimage/filter/canny.py | 26 +- skimage/filter/edges.py | 7 +- skimage/filter/lpi_filter.py | 29 ++- skimage/filter/rank_order.py | 11 +- skimage/filter/setup.py | 13 +- skimage/filter/tests/test_edges.py | 7 +- skimage/filter/tests/test_lpi_filter.py | 26 +- skimage/filter/tests/test_thresholding.py | 4 +- skimage/filter/tests/test_tv_denoise.py | 16 +- skimage/filter/thresholding.py | 3 +- skimage/filter/tv_denoise.py | 93 +++---- skimage/graph/mcp.py | 1 + skimage/graph/setup.py | 11 +- skimage/graph/spath.py | 7 +- skimage/graph/tests/test_heap.py | 32 +-- skimage/graph/tests/test_mcp.py | 92 +++---- skimage/graph/tests/test_spath.py | 3 + skimage/io/_io.py | 6 + skimage/io/_plugins/fits_plugin.py | 11 +- skimage/io/_plugins/freeimage_plugin.py | 241 ++++++++++-------- skimage/io/_plugins/gdal_plugin.py | 2 +- skimage/io/_plugins/matplotlib_plugin.py | 2 + skimage/io/_plugins/pil_plugin.py | 5 + skimage/io/_plugins/plugin.py | 13 +- skimage/io/_plugins/q_color_mixer.py | 18 +- skimage/io/_plugins/q_histogram.py | 15 +- skimage/io/_plugins/skivi.py | 16 +- skimage/io/_plugins/test_plugin.py | 4 + skimage/io/_plugins/util.py | 45 ++-- skimage/io/collection.py | 6 +- skimage/io/setup.py | 11 +- skimage/io/sift.py | 3 + skimage/io/tests/test_collection.py | 10 +- skimage/io/tests/test_colormixer.py | 2 - skimage/io/tests/test_fits.py | 10 +- skimage/io/tests/test_freeimage.py | 17 +- skimage/io/tests/test_histograms.py | 9 +- skimage/io/tests/test_io.py | 2 + skimage/io/tests/test_pil.py | 6 + skimage/io/tests/test_plugin.py | 4 +- skimage/io/tests/test_plugin_util.py | 5 +- skimage/io/tests/test_sift.py | 2 + skimage/io/video.py | 96 +++---- skimage/measure/_regionprops.py | 25 +- skimage/measure/find_contours.py | 20 +- skimage/measure/setup.py | 11 +- skimage/measure/tests/test_find_contours.py | 9 +- skimage/measure/tests/test_regionprops.py | 27 +- skimage/morphology/convex_hull.py | 5 +- skimage/morphology/grey.py | 10 +- skimage/morphology/selem.py | 22 +- skimage/morphology/setup.py | 13 +- skimage/morphology/skeletonize.py | 114 +++++---- skimage/morphology/tests/test_convex_hull.py | 2 + skimage/morphology/tests/test_grey.py | 2 +- skimage/morphology/tests/test_pnpoly.py | 4 +- skimage/morphology/tests/test_selem.py | 6 +- skimage/morphology/tests/test_skeletonize.py | 63 ++--- skimage/morphology/tests/test_watershed.py | 48 ++-- skimage/morphology/watershed.py | 85 +++--- skimage/scripts/skivi | 1 - skimage/scripts/skivi.py | 3 +- skimage/setup.py | 16 +- skimage/transform/_warp_zoo.py | 3 +- skimage/transform/finite_radon_transform.py | 7 +- skimage/transform/hough_transform.py | 21 +- skimage/transform/integral.py | 1 + skimage/transform/project.py | 9 +- skimage/transform/radon_transform.py | 23 +- skimage/transform/setup.py | 14 +- .../tests/test_finite_radon_transform.py | 3 +- .../transform/tests/test_hough_transform.py | 4 + skimage/transform/tests/test_integral.py | 5 +- skimage/transform/tests/test_project.py | 8 +- .../transform/tests/test_radon_transform.py | 2 + skimage/util/dtype.py | 30 +-- skimage/util/tests/test_dtype.py | 2 + 98 files changed, 1025 insertions(+), 866 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index bcdaf2b0..a48ce4b2 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -61,6 +61,7 @@ try: except ImportError: __version__ = "unbuilt-dev" + def _setup_test(verbose=False): import gzip import functools @@ -93,6 +94,7 @@ if test_verbose is None: except NameError: pass + def get_log(name=None): """Return a console logger. @@ -120,11 +122,13 @@ def get_log(name=None): log = logging.getLogger(name) return log + def _setup_log(): """Configure root logger. """ - import logging, sys + import logging + import sys log = logging.getLogger() diff --git a/skimage/_build.py b/skimage/_build.py index dbf5e678..8f255f29 100644 --- a/skimage/_build.py +++ b/skimage/_build.py @@ -1,9 +1,8 @@ import sys import os -import shutil import hashlib import subprocess -import platform + def cython(pyx_files, working_path=''): """Use Cython to convert the given files to C. @@ -39,17 +38,18 @@ def cython(pyx_files, working_path=''): print(cmd) try: - status = subprocess.call(['cython', '-o', c_file, pyxfile]) + 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. - status = subprocess.call( + subprocess.call( [sys.executable, os.path.join(os.path.dirname(sys.executable), 'Scripts', 'cython.py'), '-o', c_file, pyxfile], shell=True) + def _md5sum(f): m = hashlib.new('md5') while True: @@ -60,6 +60,7 @@ def _md5sum(f): m.update(d) return m.hexdigest() + def _changed(filename): """Compare the hash of a Cython file to the cached hash value on disk. diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 36165d68..274581b5 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -485,7 +485,7 @@ def rgb2grey(rgb): Raises ------ ValueError - If `rgb2grey` is not a 3-D array of shape (.., .., 3) or + If `rgb2grey` is not a 3-D array of shape (.., .., 3) or (.., .., 4). References diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index be0d330c..b053d470 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -57,8 +57,7 @@ class TestColorconv(TestCase): self.assertRaises(ValueError, rgb2hsv, self.img_grayscale) def test_rgb2hsv_error_one_element(self): - self.assertRaises(ValueError, rgb2hsv, self.img_rgb[0,0]) - + self.assertRaises(ValueError, rgb2hsv, self.img_rgb[0, 0]) # HSV to RGB def test_hsv2rgb_conversion(self): @@ -74,19 +73,18 @@ class TestColorconv(TestCase): self.assertRaises(ValueError, hsv2rgb, self.img_grayscale) def test_hsv2rgb_error_one_element(self): - self.assertRaises(ValueError, hsv2rgb, self.img_rgb[0,0]) - + self.assertRaises(ValueError, hsv2rgb, self.img_rgb[0, 0]) # RGB to XYZ def test_rgb2xyz_conversion(self): - gt = np.array([[[ 0.950456, 1. , 1.088754], - [ 0.538003, 0.787329, 1.06942 ], - [ 0.592876, 0.28484 , 0.969561], - [ 0.180423, 0.072169, 0.950227]], - [[ 0.770033, 0.927831, 0.138527], - [ 0.35758 , 0.71516 , 0.119193], - [ 0.412453, 0.212671, 0.019334], - [ 0. , 0. , 0. ]]]) + gt = np.array([[[0.950456, 1., 1.088754], + [0.538003, 0.787329, 1.06942], + [0.592876, 0.28484, 0.969561], + [0.180423, 0.072169, 0.950227]], + [[0.770033, 0.927831, 0.138527], + [0.35758, 0.71516, 0.119193], + [0.412453, 0.212671, 0.019334], + [0., 0., 0.]]]) assert_almost_equal(rgb2xyz(self.colbars_array), gt) # stop repeating the "raises" checks for all other functions that are @@ -95,8 +93,7 @@ class TestColorconv(TestCase): self.assertRaises(ValueError, rgb2xyz, self.img_grayscale) def test_rgb2xyz_error_one_element(self): - self.assertRaises(ValueError, rgb2xyz, self.img_rgb[0,0]) - + self.assertRaises(ValueError, rgb2xyz, self.img_rgb[0, 0]) # XYZ to RGB def test_xyz2rgb_conversion(self): @@ -104,20 +101,18 @@ class TestColorconv(TestCase): assert_almost_equal(xyz2rgb(rgb2xyz(self.colbars_array)), self.colbars_array) - # RGB to RGB CIE def test_rgb2rgbcie_conversion(self): - gt = np.array([[[ 0.1488856 , 0.18288098, 0.19277574], - [ 0.01163224, 0.16649536, 0.18948516], - [ 0.12259182, 0.03308008, 0.17298223], + gt = np.array([[[0.1488856, 0.18288098, 0.19277574], + [0.01163224, 0.16649536, 0.18948516], + [0.12259182, 0.03308008, 0.17298223], [-0.01466154, 0.01669446, 0.16969164]], - [[ 0.16354714, 0.16618652, 0.0230841 ], - [ 0.02629378, 0.1498009 , 0.01979351], - [ 0.13725336, 0.01638562, 0.00329059], - [ 0. , 0. , 0. ]]]) + [[0.16354714, 0.16618652, 0.0230841], + [0.02629378, 0.1498009, 0.01979351], + [0.13725336, 0.01638562, 0.00329059], + [0., 0., 0.]]]) assert_almost_equal(rgb2rgbcie(self.colbars_array), gt) - # RGB CIE to RGB def test_rgbcie2rgb_conversion(self): # only roundtrip test, we checked rgb2rgbcie above already @@ -151,6 +146,7 @@ class TestColorconv(TestCase): assert_equal(g.shape, (1, 1)) + def test_gray2rgb(): x = np.array([0, 0.5, 1]) assert_raises(ValueError, gray2rgb, x) @@ -170,4 +166,3 @@ def test_gray2rgb(): if __name__ == "__main__": run_module_suite() - diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index c4467678..5a01a493 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -11,6 +11,7 @@ import os as _os from ..io import imread from skimage import data_dir + def load(f): """Load an image file located in the data directory. @@ -26,6 +27,7 @@ def load(f): """ return imread(_os.path.join(data_dir, f)) + def camera(): """Gray-level "camera" image, often used for segmentation and denoising examples. @@ -33,6 +35,7 @@ def camera(): """ return load("camera.png") + def lena(): """Colour "Lena" image. @@ -44,6 +47,7 @@ def lena(): """ return load("lena.png") + def text(): """ Gray-level "text" image used for corner detection. @@ -68,6 +72,7 @@ def checkerboard(): """ return load("chessboard_GRAY.png") + def coins(): """Greek coins from Pompeii. @@ -88,6 +93,7 @@ def coins(): """ return load("coins.png") + def moon(): """Surface of the moon. @@ -97,6 +103,7 @@ def moon(): """ return load("moon.png") + def page(): """Scanned page. diff --git a/skimage/data/tests/test_data.py b/skimage/data/tests/test_data.py index c239d880..c4c57f86 100644 --- a/skimage/data/tests/test_data.py +++ b/skimage/data/tests/test_data.py @@ -2,16 +2,19 @@ import skimage.data as data from numpy.testing import assert_equal, assert_array_equal import numpy as np + def test_lena(): """ Test that "Lena" image can be loaded. """ lena = data.lena() assert_equal(lena.shape, (512, 512, 3)) + def test_camera(): """ Test that "camera" image can be loaded. """ cameraman = data.camera() assert_equal(cameraman.ndim, 2) + def test_checkerboard(): """ Test that checkerboard image can be loaded. """ checkerboard = data.checkerboard() @@ -19,4 +22,3 @@ def test_checkerboard(): if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() - diff --git a/skimage/draw/setup.py b/skimage/draw/setup.py index d296fee8..4503d664 100644 --- a/skimage/draw/setup.py +++ b/skimage/draw/setup.py @@ -5,6 +5,7 @@ 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 @@ -20,11 +21,11 @@ def configuration(parent_package='', top_path=None): if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image developers', - author = 'scikits-image developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Drawing', - url = 'https://github.com/scikits-image/scikits-image', - license = 'SciPy License (BSD Style)', + setup(maintainer='scikits-image developers', + author='scikits-image developers', + maintainer_email='scikits-image@googlegroups.com', + description='Drawing', + url='https://github.com/scikits-image/scikits-image', + license='SciPy License (BSD Style)', **(configuration(top_path='').todict()) ) diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index dd1c81e2..9e6ca8e8 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -15,6 +15,7 @@ def test_line_horizontal(): assert_array_equal(img, img_) + def test_line_vertical(): img = np.zeros((10, 10)) @@ -26,6 +27,7 @@ def test_line_vertical(): assert_array_equal(img, img_) + def test_line_reverse(): img = np.zeros((10, 10)) @@ -37,6 +39,7 @@ def test_line_reverse(): assert_array_equal(img, img_) + def test_line_diag(): img = np.zeros((5, 5)) @@ -52,20 +55,21 @@ def test_polygon_rectangle(): img = np.zeros((10, 10), 'uint8') poly = np.array(((1, 1), (4, 1), (4, 4), (1, 4), (1, 1))) - rr, cc = polygon(poly[:,0], poly[:,1]) - img[rr,cc] = 1 + rr, cc = polygon(poly[:, 0], poly[:, 1]) + img[rr, cc] = 1 img_ = np.zeros((10, 10)) - img_[1:4,1:4] = 1 + img_[1:4, 1:4] = 1 assert_array_equal(img, img_) + def test_polygon_rectangle_angular(): img = np.zeros((10, 10), 'uint8') poly = np.array(((0, 3), (4, 7), (7, 4), (3, 0), (0, 3))) - rr, cc = polygon(poly[:,0], poly[:,1]) - img[rr,cc] = 1 + rr, cc = polygon(poly[:, 0], poly[:, 1]) + img[rr, cc] = 1 img_ = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -82,12 +86,13 @@ def test_polygon_rectangle_angular(): assert_array_equal(img, img_) + def test_polygon_parallelogram(): img = np.zeros((10, 10), 'uint8') poly = np.array(((1, 1), (5, 1), (7, 6), (3, 6), (1, 1))) - rr, cc = polygon(poly[:,0], poly[:,1]) - img[rr,cc] = 1 + rr, cc = polygon(poly[:, 0], poly[:, 1]) + img[rr, cc] = 1 img_ = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -104,23 +109,25 @@ def test_polygon_parallelogram(): assert_array_equal(img, img_) + def test_polygon_exceed(): img = np.zeros((10, 10), 'uint8') poly = np.array(((1, -1), (100, -1), (100, 100), (1, 100), (1, 1))) - rr, cc = polygon(poly[:,0], poly[:,1], img.shape) - img[rr,cc] = 1 + rr, cc = polygon(poly[:, 0], poly[:, 1], img.shape) + img[rr, cc] = 1 img_ = np.zeros((10, 10)) - img_[1:,:] = 1 + img_[1:, :] = 1 assert_array_equal(img, img_) + def test_circle(): img = np.zeros((15, 15), 'uint8') rr, cc = circle(7, 7, 6) - img[rr,cc] = 1 + img[rr, cc] = 1 img_ = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -142,11 +149,12 @@ def test_circle(): assert_array_equal(img, img_) + def test_ellipse(): img = np.zeros((15, 15), 'uint8') rr, cc = ellipse(7, 7, 3, 7) - img[rr,cc] = 1 + img[rr, cc] = 1 img_ = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index a0a576d6..5a722308 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -183,4 +183,3 @@ def rescale_intensity(image, in_range=None, out_range=None): image = (image - imin) / float(imax - imin) return dtype(image * (omax - omin) + omin) - diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index 55fae5ae..b6ed0b12 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -74,4 +74,3 @@ def test_rescale_out_range(): if __name__ == '__main__': from numpy import testing testing.run_module_suite() - diff --git a/skimage/feature/greycomatrix.py b/skimage/feature/greycomatrix.py index 5b4bdb85..34e05352 100644 --- a/skimage/feature/greycomatrix.py +++ b/skimage/feature/greycomatrix.py @@ -4,7 +4,6 @@ properties to characterize image textures. """ import numpy as np -import skimage.util from ._greycomatrix import _glcm_loop @@ -28,17 +27,17 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, 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. + (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. + 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 + 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 + offset. The elements of the resulting matrix sum to 1. The default is False. Returns @@ -54,10 +53,10 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, ---------- .. [1] The GLCM Tutorial Home Page, http://www.fp.ucalgary.ca/mhallbey/tutorial.htm - .. [2] Pattern Recognition Engineering, Morton Nadler & Eric P. + .. [2] Pattern Recognition Engineering, Morton Nadler & Eric P. Smith .. [3] Wikipedia, http://en.wikipedia.org/wiki/Co-occurrence_matrix - + Examples -------- @@ -74,7 +73,7 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, [0, 2, 0, 0], [0, 0, 3, 1], [0, 0, 0, 1]], dtype=uint32) - >>> result[:, :, 0, 1] + >>> result[:, :, 0, 1] array([[3, 0, 2, 0], [0, 2, 2, 0], [0, 0, 1, 2], @@ -82,7 +81,7 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, """ - assert levels <= 256 + assert levels <= 256 image = np.ascontiguousarray(image) assert image.ndim == 2 assert image.min() >= 0 @@ -95,7 +94,7 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, P = np.zeros((levels, levels, len(distances), len(angles)), dtype=np.uint32, order='C') - + # count co-occurences _glcm_loop(image, distances, angles, levels, P) @@ -103,8 +102,7 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, if symmetric: Pt = np.transpose(P, (1, 0, 2, 3)) P = P + Pt - - + # normalize each GLMC if normed: P = P.astype(np.float64) @@ -117,25 +115,25 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, def greycoprops(P, prop='contrast'): """Calculate texture properties of a GLCM. - - Compute a feature of a grey level co-occurrence matrix to serve as + + 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` + - '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) \\ + .. 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 + 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 @@ -144,42 +142,42 @@ def greycoprops(P, prop='contrast'): 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 + 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 - + http://www.fp.ucalgary.ca/mhallbey/tutorial.htm + Examples -------- Compute the contrast for GLCMs with distances [1, 2] and angles - [0 degrees, 90 degrees] - + [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, + >>> 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': @@ -193,7 +191,7 @@ def greycoprops(P, prop='contrast'): else: raise ValueError('%s is an invalid property' % (prop)) - # compute property for each GLCM + # 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) @@ -205,19 +203,19 @@ def greycoprops(P, prop='contrast'): 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), + + 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), + 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)), + 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]) diff --git a/skimage/feature/harris.py b/skimage/feature/harris.py index 6d0da9c0..e496b33d 100644 --- a/skimage/feature/harris.py +++ b/skimage/feature/harris.py @@ -76,7 +76,7 @@ def harris(image, min_distance=10, threshold=0.1, eps=1e-6, ------- coordinates : (N, 2) array (row, column) coordinates of interest points. - + Examples ------- >>> square = np.zeros([10,10]) @@ -93,18 +93,17 @@ def harris(image, min_distance=10, threshold=0.1, eps=1e-6, [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]) >>> harris(square, min_distance=1) - + Corners of the square - + array([[3, 3], [3, 6], [6, 3], - [6, 6]]) + [6, 6]]) """ - + harrisim = _compute_harris_response(image, eps=eps, gaussian_deviation=gaussian_deviation) coordinates = peak.peak_local_max(harrisim, min_distance=min_distance, threshold=threshold) return coordinates - diff --git a/skimage/feature/hog.py b/skimage/feature/hog.py index fc5c19da..e0e1301d 100644 --- a/skimage/feature/hog.py +++ b/skimage/feature/hog.py @@ -2,6 +2,7 @@ import numpy as np from scipy import sqrt, pi, arctan2, cos, sin from scipy.ndimage import uniform_filter + def hog(image, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(3, 3), visualise=False, normalise=False): """Extract Histogram of Oriented Gradients (HOG) for a given image. @@ -118,12 +119,11 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), cond2 = temp_ori > 0 temp_mag = np.where(cond2, magnitude, 0) - orientation_histogram[:,:,i] = uniform_filter(temp_mag, size=(cy, cx))[cy/2::cy, cx/2::cx] - + orientation_histogram[:, :, i] = uniform_filter(temp_mag, + size=(cy, cx))[cy / 2::cy, cx / 2::cx] # now for each cell, compute the histogram #orientation_histogram = np.zeros((n_cellsx, n_cellsy, orientations)) - radius = min(cx, cy) // 2 - 1 hog_image = None if visualise: @@ -131,7 +131,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), if visualise: from skimage import draw - + for x in range(n_cellsx): for y in range(n_cellsy): for o in range(orientations): diff --git a/skimage/feature/peak.py b/skimage/feature/peak.py index 5d3f375b..d8e54dd6 100644 --- a/skimage/feature/peak.py +++ b/skimage/feature/peak.py @@ -38,15 +38,15 @@ def peak_local_max(image, min_distance=10, threshold='deprecated', ------- coordinates : (N, 2) array (row, column) coordinates of peaks. - + Notes ----- The peak local maximum function returns the coordinates of local peaks (maxima) in a image. A maximum filter is used for finding local maxima. This operation - dilates the original image. After comparison between dilated and original image, + dilates the original image. After comparison between dilated and original image, peak_local_max function returns the coordinates of peaks where dilated image = original. - + Examples -------- >>> im = np.zeros((7, 7)) @@ -60,14 +60,14 @@ def peak_local_max(image, min_distance=10, threshold='deprecated', [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. , 0. , 0. , 0. ]]) - + >>> peak_local_max(im, min_distance=1) array([[3, 2], [3, 4]]) - + >>> peak_local_max(im, min_distance=2) array([[3, 2]]) - + """ if np.all(image == image.flat[0]): return [] @@ -101,4 +101,3 @@ def peak_local_max(image, min_distance=10, threshold='deprecated', coordinates = coordinates[idx_maxsort][:2] return coordinates - diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index 13d4fae5..39358fd3 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -5,6 +5,7 @@ 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 @@ -23,11 +24,11 @@ def configuration(parent_package='', top_path=None): if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - author = 'scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Features', - url = 'https://github.com/scikits-image/scikits-image', - license = 'SciPy License (BSD Style)', + setup(maintainer='scikits-image Developers', + author='scikits-image Developers', + maintainer_email='scikits-image@googlegroups.com', + description='Features', + url='https://github.com/scikits-image/scikits-image', + license='SciPy License (BSD Style)', **(configuration(top_path='').todict()) ) diff --git a/skimage/feature/template.py b/skimage/feature/template.py index 4f3449bd..500f02a0 100644 --- a/skimage/feature/template.py +++ b/skimage/feature/template.py @@ -77,8 +77,7 @@ def match_template(image, template, pad_input=False): i0, j0 = template.shape i0 /= 2 j0 /= 2 - pad_image[i0:i0+h, j0:j0+w] = image + pad_image[i0:i0 + h, j0:j0 + w] = image image = pad_image result = _template.match_template(image, template) return result - diff --git a/skimage/feature/tests/test_glcm.py b/skimage/feature/tests/test_glcm.py index 3f321e55..da90ec56 100644 --- a/skimage/feature/tests/test_glcm.py +++ b/skimage/feature/tests/test_glcm.py @@ -7,8 +7,8 @@ class TestGLCM(): self.image = np.array([[0, 0, 1, 1], [0, 0, 1, 1], [0, 2, 2, 2], - [2, 2, 3, 3]], dtype=np.uint8) - + [2, 2, 3, 3]], dtype=np.uint8) + def test_output_angles(self): result = greycomatrix(self.image, [1], [0, np.pi / 2], 4) assert result.shape == (4, 4, 1, 2) @@ -21,23 +21,23 @@ class TestGLCM(): [0, 2, 2, 0], [0, 0, 1, 2], [0, 0, 0, 0]], dtype=np.uint32) - np.testing.assert_array_equal(result[:, :, 0, 1], expected2) - - def test_output_symmetric_1(self): - result = greycomatrix(self.image, [1], [np.pi / 2], 4, + np.testing.assert_array_equal(result[:, :, 0, 1], expected2) + + def test_output_symmetric_1(self): + result = greycomatrix(self.image, [1], [np.pi / 2], 4, symmetric=True) assert result.shape == (4, 4, 1, 1) expected = np.array([[6, 0, 2, 0], [0, 4, 2, 0], [2, 2, 2, 2], [0, 0, 2, 0]], dtype=np.uint32) - np.testing.assert_array_equal(result[:, :, 0, 0], expected) + np.testing.assert_array_equal(result[:, :, 0, 0], expected) def test_output_distance(self): im = np.array([[0, 0, 0, 0], [1, 0, 0, 1], [2, 0, 0, 2], - [3, 0, 0, 3]], dtype=np.uint8) + [3, 0, 0, 3]], dtype=np.uint8) result = greycomatrix(im, [3], [0], 4, symmetric=False) expected = np.array([[1, 0, 0, 0], [0, 1, 0, 0], @@ -52,7 +52,7 @@ class TestGLCM(): [3]], dtype=np.uint8) result = greycomatrix(im, [1, 2], [0, np.pi / 2], 4) assert result.shape == (4, 4, 2, 2) - + z = np.zeros((4, 4), dtype=np.uint32) e1 = np.array([[0, 1, 0, 0], [0, 0, 1, 0], @@ -62,7 +62,7 @@ class TestGLCM(): [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], dtype=np.uint32) - + np.testing.assert_array_equal(result[:, :, 0, 0], z) np.testing.assert_array_equal(result[:, :, 1, 0], z) np.testing.assert_array_equal(result[:, :, 0, 1], e1) @@ -70,39 +70,39 @@ class TestGLCM(): def test_output_empty(self): result = greycomatrix(self.image, [10], [0], 4) - np.testing.assert_array_equal(result[:, :, 0, 0], - np.zeros((4, 4), dtype=np.uint32)) + np.testing.assert_array_equal(result[:, :, 0, 0], + np.zeros((4, 4), dtype=np.uint32)) result = greycomatrix(self.image, [10], [0], 4, normed=True) - np.testing.assert_array_equal(result[:, :, 0, 0], - np.zeros((4, 4), dtype=np.uint32)) + np.testing.assert_array_equal(result[:, :, 0, 0], + np.zeros((4, 4), dtype=np.uint32)) - def test_normed_symmetric(self): - result = greycomatrix(self.image, [1, 2, 3], - [0, np.pi / 2, np.pi], 4, + def test_normed_symmetric(self): + result = greycomatrix(self.image, [1, 2, 3], + [0, np.pi / 2, np.pi], 4, normed=True, symmetric=True) for d in range(result.shape[2]): for a in range(result.shape[3]): - np.testing.assert_almost_equal(result[:, :, d, a].sum(), + np.testing.assert_almost_equal(result[:, :, d, a].sum(), 1.0) - np.testing.assert_array_equal(result[:, :, d, a], + np.testing.assert_array_equal(result[:, :, d, a], result[:, :, d, a].transpose()) def test_contrast(self): - result = greycomatrix(self.image, [1, 2], [0], 4, + result = greycomatrix(self.image, [1, 2], [0], 4, normed=True, symmetric=True) result = np.round(result, 3) contrast = greycoprops(result, 'contrast') np.testing.assert_almost_equal(contrast[0, 0], 0.586) - + def test_dissimilarity(self): - result = greycomatrix(self.image, [1], [0, np.pi / 2], 4, + result = greycomatrix(self.image, [1], [0, np.pi / 2], 4, normed=True, symmetric=True) result = np.round(result, 3) dissimilarity = greycoprops(result, 'dissimilarity') np.testing.assert_almost_equal(dissimilarity[0, 0], 0.418) def test_dissimilarity_2(self): - result = greycomatrix(self.image, [1, 3], [np.pi/2], 4, + result = greycomatrix(self.image, [1, 3], [np.pi / 2], 4, normed=True, symmetric=True) result = np.round(result, 3) dissimilarity = greycoprops(result, 'dissimilarity')[0, 0] @@ -110,23 +110,23 @@ class TestGLCM(): def test_invalid_property(self): result = greycomatrix(self.image, [1], [0], 4) - np.testing.assert_raises(ValueError, greycoprops, + np.testing.assert_raises(ValueError, greycoprops, result, 'ABC') - + def test_homogeneity(self): - result = greycomatrix(self.image, [1], [0, 6], 4, normed=True, + result = greycomatrix(self.image, [1], [0, 6], 4, normed=True, symmetric=True) homogeneity = greycoprops(result, 'homogeneity')[0, 0] np.testing.assert_almost_equal(homogeneity, 0.80833333) def test_energy(self): - result = greycomatrix(self.image, [1], [0, 4], 4, normed=True, + result = greycomatrix(self.image, [1], [0, 4], 4, normed=True, symmetric=True) energy = greycoprops(result, 'energy')[0, 0] np.testing.assert_almost_equal(energy, 0.38188131) - + def test_correlation(self): - result = greycomatrix(self.image, [1, 2], [0], 4, normed=True, + result = greycomatrix(self.image, [1, 2], [0], 4, normed=True, symmetric=True) energy = greycoprops(result, 'correlation') np.testing.assert_almost_equal(energy[0, 0], 0.71953255) @@ -134,9 +134,9 @@ class TestGLCM(): def test_uniform_properties(self): im = np.ones((4, 4), dtype=np.uint8) - result = greycomatrix(im, [1, 2, 8], [0, np.pi / 2], 4, normed=True, + result = greycomatrix(im, [1, 2, 8], [0, np.pi / 2], 4, normed=True, symmetric=True) - for prop in ['contrast', 'dissimilarity', 'homogeneity', + for prop in ['contrast', 'dissimilarity', 'homogeneity', 'energy', 'correlation', 'ASM']: greycoprops(result, prop) diff --git a/skimage/feature/tests/test_harris.py b/skimage/feature/tests/test_harris.py index 758bfa5e..43bf28a3 100644 --- a/skimage/feature/tests/test_harris.py +++ b/skimage/feature/tests/test_harris.py @@ -13,6 +13,7 @@ def test_square_image(): assert results.any() assert len(results) == 1 + def test_noisy_square_image(): im = np.zeros((50, 50)).astype(float) im[:25, :25] = 1. @@ -21,6 +22,7 @@ def test_noisy_square_image(): assert results.any() assert len(results) == 1 + def test_squared_dot(): im = np.zeros((50, 50)) im[4:8, 4:8] = 1 @@ -28,6 +30,7 @@ def test_squared_dot(): results = harris(im, min_distance=3) assert (results == np.array([[6, 6]])).all() + def test_rotated_lena(): """ The harris filter should yield the same results with an image and it's @@ -44,4 +47,3 @@ def test_rotated_lena(): if __name__ == '__main__': from numpy import testing testing.run_module_suite() - diff --git a/skimage/feature/tests/test_hog.py b/skimage/feature/tests/test_hog.py index e4b8fda8..18c13c85 100644 --- a/skimage/feature/tests/test_hog.py +++ b/skimage/feature/tests/test_hog.py @@ -1,17 +1,18 @@ import numpy as np import scipy -from skimage.feature import hog +from skimage.feature import hog + def test_histogram_of_oriented_gradients(): # Replace with skimage.data.lena() after merge - img = scipy.misc.lena()[:256,:].astype(np.int8) - - fd = hog(img, orientations=9, pixels_per_cell=(8, 8), + img = scipy.misc.lena()[:256, :].astype(np.int8) + + fd = hog(img, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(1, 1)) - assert len(fd) == 9 * (256//8) * (512//8) - + assert len(fd) == 9 * (256 // 8) * (512 // 8) + if __name__ == '__main__': from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/feature/tests/test_peak.py b/skimage/feature/tests/test_peak.py index 91d38d99..eaedc84f 100644 --- a/skimage/feature/tests/test_peak.py +++ b/skimage/feature/tests/test_peak.py @@ -65,4 +65,3 @@ def test_num_peaks(): 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 7097a70a..1b9ff213 100644 --- a/skimage/feature/tests/test_template.py +++ b/skimage/feature/tests/test_template.py @@ -50,8 +50,8 @@ def test_normalization(): image[ineg:ineg + n, jneg:jneg + n] = 0 # white square with a black border - template = np.zeros((n+2, n+2)) - template[1:1+n, 1:1+n] = 1 + template = np.zeros((n + 2, n + 2)) + template[1:1 + n, 1:1 + n] = 1 result = match_template(image, template) @@ -121,4 +121,3 @@ def test_pad_input(): if __name__ == "__main__": from numpy import testing testing.run_module_suite() - diff --git a/skimage/filter/canny.py b/skimage/filter/canny.py index 4818916c..8be77894 100644 --- a/skimage/filter/canny.py +++ b/skimage/filter/canny.py @@ -56,7 +56,7 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): Parameters ----------- image : array_like, dtype=float - The greyscale input image to detect edges on; should be normalized to + The greyscale input image to detect edges on; should be normalized to 0.0 to 1.0. sigma : float @@ -85,21 +85,21 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): The steps of the algorithm are as follows: * Smooth the image using a Gaussian with ``sigma`` width. - + * Apply the horizontal and vertical Sobel operators to get the gradients within the image. The edge strength is the norm of the gradient. - - * Thin potential edges to 1-pixel wide curves. First, find the normal - to the edge at each point. This is done by looking at the + + * Thin potential edges to 1-pixel wide curves. First, find the normal + to the edge at each point. This is done by looking at the signs and the relative magnitude of the X-Sobel and Y-Sobel to sort the points into 4 categories: horizontal, vertical, - diagonal and antidiagonal. Then look in the normal and reverse - directions to see if the values in either of those directions are - greater than the point in question. Use interpolation to get a mix of - points instead of picking the one that's the closest to the normal. - - * Perform a hysteresis thresholding: first label all points above the - high threshold as edges. Then recursively label any point above the + diagonal and antidiagonal. Then look in the normal and reverse + directions to see if the values in either of those directions are + greater than the point in question. Use interpolation to get a mix of + points instead of picking the one that's the closest to the normal. + + * Perform a hysteresis thresholding: first label all points above the + high threshold as edges. Then recursively label any point above the low threshold that is 8-connected to a labeled point as an edge. References @@ -120,7 +120,7 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): >>> # First trial with the Canny filter, with the default smoothing >>> edges1 = filter.canny(im) >>> # Increase the smoothing for better results - >>> edges2 = filter.canny(im, sigma=3) + >>> edges2 = filter.canny(im, sigma=3) ''' # diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index f6bf7031..6ceb5c5f 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -12,6 +12,7 @@ import numpy as np from skimage import img_as_float from scipy.ndimage import convolve, binary_erosion, generate_binary_structure + def sobel(image, mask=None): """Calculate the absolute magnitude Sobel to find edges. @@ -36,7 +37,8 @@ def sobel(image, mask=None): Note that ``scipy.ndimage.sobel`` returns a directional Sobel which has to be further processed to perform edge detection. """ - return np.sqrt(hsobel(image, mask)**2 + vsobel(image, mask)**2) + return np.sqrt(hsobel(image, mask) ** 2 + vsobel(image, mask) ** 2) + def hsobel(image, mask=None): """Find the horizontal edges of an image using the Sobel transform. @@ -114,6 +116,7 @@ def vsobel(image, mask=None): result[big_mask == False] = 0 return result + def prewitt(image, mask=None): """Find the edge magnitude using the Prewitt transform. @@ -136,6 +139,7 @@ def prewitt(image, mask=None): """ return np.sqrt(hprewitt(image, mask) ** 2 + vprewitt(image, mask) ** 2) + def hprewitt(image, mask=None): """Find the horizontal edges of an image using the Prewitt transform. @@ -174,6 +178,7 @@ def hprewitt(image, mask=None): result[big_mask == False] = 0 return result + def vprewitt(image, mask=None): """Find the vertical edges of an image using the Prewitt transform. diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index 50d2de17..92490abe 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -11,10 +11,12 @@ from scipy.fftpack import fftshift, ifftshift eps = np.finfo(float).eps + def _min_limit(x, val=eps): mask = np.abs(x) < eps x[mask] = np.sign(x[mask]) * eps + def _centre(x, oshape): """Return an array of oshape from the centre of x. @@ -23,6 +25,7 @@ def _centre(x, oshape): out = x[[slice(s, s + n) for s, n in zip(start, oshape)]] return out + def _pad(data, shape): """Pad the data to the given shape with zeros. @@ -38,7 +41,6 @@ def _pad(data, shape): return out - class LPIFilter2D(object): """Linear Position-Invariant Filter (2-dimensional) @@ -83,21 +85,21 @@ class LPIFilter2D(object): """ dshape = np.array(data.shape) - dshape += (dshape % 2 == 0) # all filter dimensions must be uneven + dshape += (dshape % 2 == 0) # all filter dimensions must be uneven oshape = np.array(data.shape) * 2 - 1 if self._cache is None or np.any(self._cache.shape != oshape): coords = np.mgrid[[slice(0, float(n)) for n in dshape]] # this steps over two sets of coordinates, # not over the coordinates individually - for k,coord in enumerate(coords): - coord -= (dshape[k] - 1)/2. - coords = coords.reshape(2, -1).T # coordinate pairs (r,c) + for k, coord in enumerate(coords): + coord -= (dshape[k] - 1) / 2. + coords = coords.reshape(2, -1).T # coordinate pairs (r,c) - f = self.impulse_response(coords[:,0],coords[:,1], + f = self.impulse_response(coords[:, 0], coords[:, 1], **self.filter_params).reshape(dshape) - f = _pad(f,oshape) + f = _pad(f, oshape) F = np.dual.fftn(f) self._cache = F else: @@ -108,7 +110,7 @@ class LPIFilter2D(object): return F, G - def __call__(self,data): + def __call__(self, data): """Apply the filter to the given data. *Parameters*: @@ -120,6 +122,7 @@ class LPIFilter2D(object): out = np.abs(_centre(out, data.shape)) return out + def forward(data, impulse_response=None, filter_params={}, predefined_filter=None): """Apply the given filter to data. @@ -155,6 +158,7 @@ def forward(data, impulse_response=None, filter_params={}, predefined_filter = LPIFilter2D(impulse_response, **filter_params) return predefined_filter(data) + def inverse(data, impulse_response=None, filter_params={}, max_gain=2, predefined_filter=None): """Apply the filter in reverse to the given data. @@ -189,12 +193,13 @@ def inverse(data, impulse_response=None, filter_params={}, max_gain=2, F, G = filt._prepare(data) _min_limit(F) - F = 1/F + F = 1 / F mask = np.abs(F) > max_gain F[mask] = np.sign(F[mask]) * max_gain return _centre(np.abs(ifftshift(np.dual.ifftn(G * F))), data.shape) + def wiener(data, impulse_response=None, filter_params={}, K=0.25, predefined_filter=None): """Minimum Mean Square Error (Wiener) inverse filter. @@ -227,12 +232,12 @@ def wiener(data, impulse_response=None, filter_params={}, K=0.25, F, G = filt._prepare(data) _min_limit(F) - H_mag_sqr = np.abs(F)**2 - F = 1/F * H_mag_sqr / (H_mag_sqr + K) + H_mag_sqr = np.abs(F) ** 2 + F = 1 / F * H_mag_sqr / (H_mag_sqr + K) return _centre(np.abs(ifftshift(np.dual.ifftn(G * F))), data.shape) + def constrained_least_squares(data, lam, impulse_response=None, filter_params={}): raise NotImplementedError - diff --git a/skimage/filter/rank_order.py b/skimage/filter/rank_order.py index 3da98974..f878702f 100644 --- a/skimage/filter/rank_order.py +++ b/skimage/filter/rank_order.py @@ -10,9 +10,10 @@ Original author: Lee Kamentstky """ import numpy + def rank_order(image): """Return an image of the same shape where each pixel is the - index of the pixel value in the ascending order of the unique + index of the pixel value in the ascending order of the unique values of `image`, aka the rank-order value. Parameters @@ -48,14 +49,12 @@ def rank_order(image): flat_image = image.ravel() sort_order = flat_image.argsort().astype(numpy.uint32) flat_image = flat_image[sort_order] - sort_rank = numpy.zeros_like(sort_order) + sort_rank = numpy.zeros_like(sort_order) is_different = flat_image[:-1] != flat_image[1:] numpy.cumsum(is_different, out=sort_rank[1:]) - original_values = numpy.zeros((sort_rank[-1]+1,),image.dtype) + original_values = numpy.zeros((sort_rank[-1] + 1,), image.dtype) original_values[0] = flat_image[0] - original_values[1:] = flat_image[1:][is_different] + original_values[1:] = flat_image[1:][is_different] int_image = numpy.zeros_like(sort_order) int_image[sort_order] = sort_rank return (int_image.reshape(image.shape), original_values) - - diff --git a/skimage/filter/setup.py b/skimage/filter/setup.py index 12cb84a7..03ea7def 100644 --- a/skimage/filter/setup.py +++ b/skimage/filter/setup.py @@ -5,6 +5,7 @@ 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 @@ -20,11 +21,11 @@ def configuration(parent_package='', top_path=None): if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - author = 'scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Filters', - url = 'https://github.com/scikits-image/scikits-image', - license = 'SciPy License (BSD Style)', + setup(maintainer='scikits-image Developers', + author='scikits-image Developers', + maintainer_email='scikits-image@googlegroups.com', + description='Filters', + url='https://github.com/scikits-image/scikits-image', + license='SciPy License (BSD Style)', **(configuration(top_path='').todict()) ) diff --git a/skimage/filter/tests/test_edges.py b/skimage/filter/tests/test_edges.py index 0c306273..bd9a2702 100644 --- a/skimage/filter/tests/test_edges.py +++ b/skimage/filter/tests/test_edges.py @@ -7,6 +7,7 @@ from scipy.ndimage import binary_dilation, binary_erosion import skimage.filter as F from skimage import data_dir, img_as_float + class TestSobel(): def test_00_00_zeros(self): """Sobel on an array of all zeros""" @@ -70,6 +71,7 @@ class TestHSobel(): result = F.hsobel(image) assert (np.all(result == 0)) + class TestVSobel(): def test_00_00_zeros(self): """Vertical sobel on an array of all zeros""" @@ -101,6 +103,7 @@ class TestVSobel(): eps = .000001 assert (np.all(np.abs(result) < eps)) + class TestPrewitt(): def test_00_00_zeros(self): """Prewitt on an array of all zeros""" @@ -132,10 +135,11 @@ class TestPrewitt(): image = (j >= 0).astype(float) result = F.prewitt(image) eps = .000001 - j[np.abs(i)==5] = 10000 + j[np.abs(i) == 5] = 10000 assert (np.all(result[j == 0] == 1)) assert (np.all(np.abs(result[np.abs(j) > 1]) < eps)) + class TestHPrewitt(): def test_00_00_zeros(self): """Horizontal sobel on an array of all zeros""" @@ -169,6 +173,7 @@ class TestHPrewitt(): eps = .000001 assert (np.all(np.abs(result) < eps)) + class TestVPrewitt(): def test_00_00_zeros(self): """Vertical prewitt on an array of all zeros""" diff --git a/skimage/filter/tests/test_lpi_filter.py b/skimage/filter/tests/test_lpi_filter.py index 1cf7b3ca..08e46a58 100644 --- a/skimage/filter/tests/test_lpi_filter.py +++ b/skimage/filter/tests/test_lpi_filter.py @@ -7,12 +7,13 @@ from skimage import data_dir from skimage.io import * from skimage.filter import * + class TestLPIFilter2D(): img = imread(os.path.join(data_dir, 'camera.png'), - flatten=True)[:50,:50] + flatten=True)[:50, :50] - def filt_func(self,r,c): - return np.exp(-np.hypot(r,c)/1) + def filt_func(self, r, c): + return np.exp(-np.hypot(r, c) / 1) def setUp(self): self.f = LPIFilter2D(self.filt_func) @@ -33,27 +34,26 @@ class TestLPIFilter2D(): g = inverse(F, predefined_filter=self.f) assert_equal(g.shape, self.img.shape) - g1 = inverse(F[::-1,::-1], predefined_filter=self.f) - assert ((g - g1[::-1,::-1]).sum() < 55) + g1 = inverse(F[::-1, ::-1], predefined_filter=self.f) + assert ((g - g1[::-1, ::-1]).sum() < 55) # test cache - g1 = inverse(F[::-1,::-1], predefined_filter=self.f) - assert ((g - g1[::-1,::-1]).sum() < 55) + g1 = inverse(F[::-1, ::-1], predefined_filter=self.f) + assert ((g - g1[::-1, ::-1]).sum() < 55) g1 = inverse(F[::-1, ::-1], self.filt_func) - assert ((g - g1[::-1,::-1]).sum() < 55) + assert ((g - g1[::-1, ::-1]).sum() < 55) def test_wiener(self): F = self.f(self.img) g = wiener(F, predefined_filter=self.f) assert_equal(g.shape, self.img.shape) - g1 = wiener(F[::-1,::-1], predefined_filter=self.f) - assert ((g - g1[::-1,::-1]).sum() < 1) + g1 = wiener(F[::-1, ::-1], predefined_filter=self.f) + assert ((g - g1[::-1, ::-1]).sum() < 1) - g1 = wiener(F[::-1,::-1], self.filt_func) - assert ((g - g1[::-1,::-1]).sum() < 1) + g1 = wiener(F[::-1, ::-1], self.filt_func) + assert ((g - g1[::-1, ::-1]).sum() < 1) if __name__ == "__main__": run_module_suite() - diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 6177b9f5..59d7e431 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -75,17 +75,19 @@ class TestSimpleImage(): def test_otsu_camera_image(): assert threshold_otsu(data.camera()) == 87 + def test_otsu_coins_image(): assert threshold_otsu(data.coins()) == 107 + def test_otsu_coins_image_as_float(): coins = skimage.img_as_float(data.coins()) assert 0.41 < threshold_otsu(coins) < 0.42 + def test_otsu_lena_image(): assert threshold_otsu(data.lena()) == 141 if __name__ == '__main__': np.testing.run_module_suite() - diff --git a/skimage/filter/tests/test_tv_denoise.py b/skimage/filter/tests/test_tv_denoise.py index f851f4f1..323c40cb 100644 --- a/skimage/filter/tests/test_tv_denoise.py +++ b/skimage/filter/tests/test_tv_denoise.py @@ -4,6 +4,7 @@ from numpy.testing import run_module_suite from skimage import filter, data, color from skimage import img_as_uint + class TestTvDenoise(): def test_tv_denoise_2d(self): @@ -14,7 +15,7 @@ class TestTvDenoise(): # lena image lena = color.rgb2gray(data.lena())[:256, :256] # add noise to lena - lena += 0.5 * lena.std()*np.random.randn(*lena.shape) + lena += 0.5 * lena.std() * np.random.randn(*lena.shape) # clip noise so that it does not exceed allowed range for float images. lena = np.clip(lena, 0, 1) # denoise @@ -22,25 +23,26 @@ class TestTvDenoise(): # which dtype? assert denoised_lena.dtype in [np.float, np.float32, np.float64] from scipy import ndimage - grad = ndimage.morphological_gradient(lena, size=((3,3))) - grad_denoised = ndimage.morphological_gradient(denoised_lena, size=((3,3))) + grad = ndimage.morphological_gradient(lena, size=((3, 3))) + grad_denoised = ndimage.morphological_gradient( + denoised_lena, size=((3, 3))) # test if the total variation has decreased - assert np.sqrt((grad_denoised**2).sum()) < np.sqrt((grad**2).sum()) / 2 + assert np.sqrt( + (grad_denoised ** 2).sum()) < np.sqrt((grad ** 2).sum()) / 2 denoised_lena_int = filter.tv_denoise(img_as_uint(lena), weight=60.0, keep_type=True) assert denoised_lena_int.dtype is np.dtype('uint16') - def test_tv_denoise_3d(self): """ Apply the TV denoising algorithm on a 3D image representing a sphere. """ x, y, z = np.ogrid[0:40, 0:40, 0:40] - mask = (x -22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 + mask = (x - 22) ** 2 + (y - 20) ** 2 + (z - 17) ** 2 < 8 ** 2 mask = 100 * mask.astype(np.float) mask += 60 - mask += 20*np.random.randn(*mask.shape) + mask += 20 * np.random.randn(*mask.shape) mask[mask < 0] = 0 mask[mask > 255] = 255 res = filter.tv_denoise(mask.astype(np.uint8), diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 4e3ebca3..505123b7 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -86,6 +86,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, return image > (thresh_image - offset) + def threshold_otsu(image, nbins=256): """Return threshold value based on Otsu's method. @@ -126,7 +127,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] diff --git a/skimage/filter/tv_denoise.py b/skimage/filter/tv_denoise.py index bb74a4bc..db45d63e 100644 --- a/skimage/filter/tv_denoise.py +++ b/skimage/filter/tv_denoise.py @@ -1,5 +1,6 @@ import numpy as np + def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): """ Perform total-variation denoising on 3-D arrays @@ -10,8 +11,8 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): 3-D input data to be denoised weight: float, optional - denoising weight. The greater ``weight``, the more denoising (at - the expense of fidelity to ``input``) + denoising weight. The greater ``weight``, the more denoising (at + the expense of fidelity to ``input``) eps: float, optional relative difference of the value of the cost function that determines @@ -29,7 +30,7 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): Notes ----- - Rudin, Osher and Fatemi algorithm + Rudin, Osher and Fatemi algorithm Examples --------- @@ -50,25 +51,25 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): i = 0 while i < n_iter_max: d = - px - py - pz - d[1:] += px[:-1] - d[:, 1:] += py[:, :-1] - d[:, :, 1:] += pz[:, :, :-1] - - out = im + d - E = (d**2).sum() + d[1:] += px[:-1] + d[:, 1:] += py[:, :-1] + d[:, :, 1:] += pz[:, :, :-1] - gx[:-1] = np.diff(out, axis=0) - gy[:, :-1] = np.diff(out, axis=1) - gz[:, :, :-1] = np.diff(out, axis=2) - norm = np.sqrt(gx**2 + gy**2 + gz**2) + out = im + d + E = (d ** 2).sum() + + gx[:-1] = np.diff(out, axis=0) + gy[:, :-1] = np.diff(out, axis=1) + gz[:, :, :-1] = np.diff(out, axis=2) + norm = np.sqrt(gx ** 2 + gy ** 2 + gz ** 2) E += weight * norm.sum() norm *= 0.5 / weight norm += 1. - px -= 1./6.*gx + px -= 1. / 6. * gx px /= norm - py -= 1./6.*gy + py -= 1. / 6. * gy py /= norm - pz -= 1/6.*gz + pz -= 1 / 6. * gz pz /= norm E /= float(im.size) if i == 0: @@ -81,7 +82,8 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): E_previous = E i += 1 return out - + + def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): """ Perform total-variation denoising @@ -92,8 +94,8 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): input data to be denoised weight: float, optional - denoising weight. The greater ``weight``, the more denoising (at - the expense of fidelity to ``input``) + denoising weight. The greater ``weight``, the more denoising (at + the expense of fidelity to ``input``) eps: float, optional relative difference of the value of the cost function that determines @@ -114,14 +116,14 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): The principle of total variation denoising is explained in http://en.wikipedia.org/wiki/Total_variation_denoising - This code is an implementation of the algorithm of Rudin, Fatemi and Osher + This code is an implementation of the algorithm of Rudin, Fatemi and Osher that was proposed by Chambolle in [1]_. References ---------- - .. [1] A. Chambolle, An algorithm for total variation minimization and - applications, Journal of Mathematical Imaging and Vision, + .. [1] A. Chambolle, An algorithm for total variation minimization and + applications, Journal of Mathematical Imaging and Vision, Springer, 2004, 20, 89-97. Examples @@ -140,21 +142,21 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): d = np.zeros_like(im) i = 0 while i < n_iter_max: - d = -px -py - d[1:] += px[:-1] - d[:, 1:] += py[:, :-1] - + d = -px - py + d[1:] += px[:-1] + d[:, 1:] += py[:, :-1] + out = im + d - E = (d**2).sum() - gx[:-1] = np.diff(out, axis=0) - gy[:, :-1] = np.diff(out, axis=1) - norm = np.sqrt(gx**2 + gy**2) + E = (d ** 2).sum() + gx[:-1] = np.diff(out, axis=0) + gy[:, :-1] = np.diff(out, axis=1) + norm = np.sqrt(gx ** 2 + gy ** 2) E += weight * norm.sum() norm *= 0.5 / weight norm += 1 - px -= 0.25*gx + px -= 0.25 * gx px /= norm - py -= 0.25*gy + py -= 0.25 * gy py /= norm E /= float(im.size) if i == 0: @@ -168,6 +170,7 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): i += 1 return out + def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): """ Perform total-variation denoising on 2-d and 3-d images @@ -176,21 +179,21 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): ---------- im: ndarray (2d or 3d) of ints, uints or floats input data to be denoised. `im` can be of any numeric type, - but it is cast into an ndarray of floats for the computation + but it is cast into an ndarray of floats for the computation of the denoised image. weight: float, optional - denoising weight. The greater ``weight``, the more denoising (at - the expense of fidelity to ``input``) + denoising weight. The greater ``weight``, the more denoising (at + the expense of fidelity to ``input``) eps: float, optional - relative difference of the value of the cost function that + relative difference of the value of the cost function that determines the stop criterion. The algorithm stops when: (E_(n-1) - E_n) < eps * E_0 keep_type: bool, optional (False) - whether the output has the same dtype as the input array. + whether the output has the same dtype as the input array. keep_type is False by default, and the dtype of the output is np.float @@ -209,19 +212,19 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): http://en.wikipedia.org/wiki/Total_variation_denoising The principle of total variation denoising is to minimize the - total variation of the image, which can be roughly described as - the integral of the norm of the image gradient. Total variation - denoising tends to produce "cartoon-like" images, that is, + total variation of the image, which can be roughly described as + the integral of the norm of the image gradient. Total variation + denoising tends to produce "cartoon-like" images, that is, piecewise-constant images. - This code is an implementation of the algorithm of Rudin, Fatemi and Osher + This code is an implementation of the algorithm of Rudin, Fatemi and Osher that was proposed by Chambolle in [1]_. References ---------- - .. [1] A. Chambolle, An algorithm for total variation minimization and - applications, Journal of Mathematical Imaging and Vision, + .. [1] A. Chambolle, An algorithm for total variation minimization and + applications, Journal of Mathematical Imaging and Vision, Springer, 2004, 20, 89-97. Examples @@ -249,9 +252,9 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): elif im.ndim == 3: out = _tv_denoise_3d(im, weight, eps, n_iter_max) else: - raise ValueError('only 2-d and 3-d images may be denoised with this function') + raise ValueError( + 'only 2-d and 3-d images may be denoised with this function') if keep_type: return out.astype(im_type) else: return out - diff --git a/skimage/graph/mcp.py b/skimage/graph/mcp.py index a803f58c..68921371 100644 --- a/skimage/graph/mcp.py +++ b/skimage/graph/mcp.py @@ -1,5 +1,6 @@ from ._mcp import MCP, MCP_Geometric, make_offsets + def route_through_array(array, start, end, fully_connected=True, geometric=True): """Simple example of how to use the MCP and MCP_Geometric classes. diff --git a/skimage/graph/setup.py b/skimage/graph/setup.py index 5e0a2f06..2c432011 100644 --- a/skimage/graph/setup.py +++ b/skimage/graph/setup.py @@ -5,6 +5,7 @@ import os.path base_path = os.path.abspath(os.path.dirname(__file__)) + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs @@ -28,10 +29,10 @@ def configuration(parent_package='', top_path=None): if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Graph-based Image-processing Algorithms', - url = 'https://github.com/scikits-image/scikits-image', - license = 'Modified BSD', + setup(maintainer='scikits-image Developers', + maintainer_email='scikits-image@googlegroups.com', + description='Graph-based Image-processing Algorithms', + url='https://github.com/scikits-image/scikits-image', + license='Modified BSD', **(configuration(top_path='').todict()) ) diff --git a/skimage/graph/spath.py b/skimage/graph/spath.py index a912a693..d8ec3526 100644 --- a/skimage/graph/spath.py +++ b/skimage/graph/spath.py @@ -1,6 +1,7 @@ import numpy as np from . import _spath + def shortest_path(arr, reach=1, axis=-1, output_indexlist=False): """Find the shortest path through an n-d array from one side to another. @@ -39,7 +40,7 @@ def shortest_path(arr, reach=1, axis=-1, output_indexlist=False): # a grid defined by the reach. if axis < 0: axis += arr.ndim - offset_ind_shape = (2*reach + 1,) * (arr.ndim - 1) + offset_ind_shape = (2 * reach + 1,) * (arr.ndim - 1) offset_indices = np.indices(offset_ind_shape) - reach offset_indices = np.insert(offset_indices, axis, np.ones(offset_ind_shape), axis=0) @@ -49,7 +50,7 @@ def shortest_path(arr, reach=1, axis=-1, output_indexlist=False): # Valid starting positions are anywhere on the hyperplane defined by # position 0 on the given axis. Ending positions are anywhere on the # hyperplane at position -1 along the same. - non_axis_shape = arr.shape[:axis] + arr.shape[axis+1:] + non_axis_shape = arr.shape[:axis] + arr.shape[axis + 1:] non_axis_indices = np.indices(non_axis_shape) non_axis_size = np.multiply.reduce(non_axis_shape) start_indices = np.insert(non_axis_indices, axis, @@ -72,7 +73,7 @@ def shortest_path(arr, reach=1, axis=-1, output_indexlist=False): if not output_indexlist: traceback = np.array(traceback) - traceback = np.concatenate([traceback[:,:axis], traceback[:,axis+1:]], + traceback = np.concatenate([traceback[:, :axis], traceback[:, axis + 1:]], axis=1) traceback = np.squeeze(traceback) diff --git a/skimage/graph/tests/test_heap.py b/skimage/graph/tests/test_heap.py index 0cc41c92..2dc7fb76 100644 --- a/skimage/graph/tests/test_heap.py +++ b/skimage/graph/tests/test_heap.py @@ -5,18 +5,20 @@ import time import random import skimage.graph.heap as heap + def test_heap(): _test_heap(100000, True) _test_heap(100000, False) + def _test_heap(n, fast_update): # generate random numbers with duplicates random.seed(0) - a = [random.uniform(1.0,100.0) for i in range(n//2)] - a = a+a - + a = [random.uniform(1.0, 100.0) for i in range(n // 2)] + a = a + a + t0 = time.clock() - + # insert in heap with random removals if fast_update: h = heap.FastUpdateBinaryHeap(128, n) @@ -25,12 +27,12 @@ def _test_heap(n, fast_update): for i in range(len(a)): h.push(a[i], i) if a[i] < 25: - # double-push same ref sometimes to test fast update codepaths - h.push(2*a[i], i) + # double-push same ref sometimes to test fast update codepaths + h.push(2 * a[i], i) if 25 < a[i] < 50: - # pop some to test random removal - h.pop() - + # pop some to test random removal + h.pop() + # pop from heap b = [] while True: @@ -38,14 +40,14 @@ def _test_heap(n, fast_update): b.append(h.pop()[0]) except IndexError: break - + t1 = time.clock() - + # verify - for i in range(1,len(b)): - assert(b[i] >= b[i-1]) - - return t1-t0 + for i in range(1, len(b)): + assert(b[i] >= b[i - 1]) + + return t1 - t0 if __name__ == "__main__": run_module_suite() diff --git a/skimage/graph/tests/test_mcp.py b/skimage/graph/tests/test_mcp.py index 9d36a6ec..f983d87f 100644 --- a/skimage/graph/tests/test_mcp.py +++ b/skimage/graph/tests/test_mcp.py @@ -3,7 +3,7 @@ from numpy.testing import * import skimage.graph.mcp as mcp -a = np.ones((8,8), dtype=np.float32) +a = np.ones((8, 8), dtype=np.float32) a[1:-1, 1] = 0 a[1, 1:-1] = 0 @@ -16,19 +16,20 @@ a[1, 1:-1] = 0 ## [ 1., 0., 1., 1., 1., 1., 1., 1.], ## [ 1., 1., 1., 1., 1., 1., 1., 1.]], dtype=float32) + def test_basic(): m = mcp.MCP(a, fully_connected=True) - costs, traceback = m.find_costs([(1,6)]) + costs, traceback = m.find_costs([(1, 6)]) return_path = m.traceback((7, 2)) assert_array_equal(costs, - [[ 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 0., 0., 0., 0., 0., 0., 1.], - [ 1., 0., 1., 1., 1., 1., 1., 1.], - [ 1., 0., 1., 2., 2., 2., 2., 2.], - [ 1., 0., 1., 2., 3., 3., 3., 3.], - [ 1., 0., 1., 2., 3., 4., 4., 4.], - [ 1., 0., 1., 2., 3., 4., 5., 5.], - [ 1., 1., 1., 2., 3., 4., 5., 6.]]) + [[1., 1., 1., 1., 1., 1., 1., 1.], + [1., 0., 0., 0., 0., 0., 0., 1.], + [1., 0., 1., 1., 1., 1., 1., 1.], + [1., 0., 1., 2., 2., 2., 2., 2.], + [1., 0., 1., 2., 3., 3., 3., 3.], + [1., 0., 1., 2., 3., 4., 4., 4.], + [1., 0., 1., 2., 3., 4., 5., 5.], + [1., 1., 1., 2., 3., 4., 5., 6.]]) assert_array_equal(return_path, [(1, 6), @@ -43,8 +44,9 @@ def test_basic(): (6, 1), (7, 2)]) + def test_neg_inf(): - expected_costs = np.where(a==1, np.inf, 0) + expected_costs = np.where(a == 1, np.inf, 0) expected_path = [(1, 6), (1, 5), (1, 4), @@ -55,8 +57,8 @@ def test_neg_inf(): (4, 1), (5, 1), (6, 1)] - test_neg = np.where(a==1, -1, 0) - test_inf = np.where(a==1, np.inf, 0) + test_neg = np.where(a == 1, -1, 0) + test_inf = np.where(a == 1, np.inf, 0) m = mcp.MCP(test_neg, fully_connected=True) costs, traceback = m.find_costs([(1, 6)]) return_path = m.traceback((6, 1)) @@ -67,11 +69,12 @@ def test_neg_inf(): return_path = m.traceback((6, 1)) assert_array_equal(costs, expected_costs) assert_array_equal(return_path, expected_path) - + def test_route(): - return_path, cost = mcp.route_through_array(a, (1,6), (7,2), geometric=True) - assert_almost_equal(cost, np.sqrt(2)/2) + return_path, cost = mcp.route_through_array( + a, (1, 6), (7, 2), geometric=True) + assert_almost_equal(cost, np.sqrt(2) / 2) assert_array_equal(return_path, [(1, 6), (1, 5), @@ -85,19 +88,20 @@ def test_route(): (6, 1), (7, 2)]) + def test_no_diagonal(): m = mcp.MCP(a, fully_connected=False) - costs, traceback = m.find_costs([(1,6)]) + costs, traceback = m.find_costs([(1, 6)]) return_path = m.traceback((7, 2)) assert_array_equal(costs, - [[ 2., 1., 1., 1., 1., 1., 1., 2.], - [ 1., 0., 0., 0., 0., 0., 0., 1.], - [ 1., 0., 1., 1., 1., 1., 1., 2.], - [ 1., 0., 1., 2., 2., 2., 2., 3.], - [ 1., 0., 1., 2., 3., 3., 3., 4.], - [ 1., 0., 1., 2., 3., 4., 4., 5.], - [ 1., 0., 1., 2., 3., 4., 5., 6.], - [ 2., 1., 2., 3., 4., 5., 6., 7.]]) + [[2., 1., 1., 1., 1., 1., 1., 2.], + [1., 0., 0., 0., 0., 0., 0., 1.], + [1., 0., 1., 1., 1., 1., 1., 2.], + [1., 0., 1., 2., 2., 2., 2., 3.], + [1., 0., 1., 2., 3., 3., 3., 4.], + [1., 0., 1., 2., 3., 4., 4., 5.], + [1., 0., 1., 2., 3., 4., 5., 6.], + [2., 1., 2., 3., 4., 5., 6., 7.]]) assert_array_equal(return_path, [(1, 6), (1, 5), @@ -115,34 +119,36 @@ def test_no_diagonal(): def test_offsets(): - offsets = [(1,i) for i in range(10)] + [(1, -i) for i in range(1,10)] - 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], - [15, 14, 13, 12, 11, 10, 0, 1], - [10, 0, 1, 2, 3, 4, 5, 6], - [10, 0, 1, 2, 3, 4, 5, 6], - [10, 0, 1, 2, 3, 4, 5, 6], - [10, 0, 1, 2, 3, 4, 5, 6], - [10, 0, 1, 2, 3, 4, 5, 6]]) - + offsets = [(1, i) for i in range(10)] + [(1, -i) for i in range(1, 10)] + 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], + [15, 14, 13, 12, 11, 10, 0, 1], + [10, 0, 1, 2, 3, 4, 5, 6], + [10, 0, 1, 2, 3, 4, 5, 6], + [10, 0, 1, 2, 3, 4, 5, 6], + [10, 0, 1, 2, 3, 4, 5, 6], + [10, 0, 1, 2, 3, 4, 5, 6]]) + def test_crashing(): - for shape in [(100, 100), (5, 8, 13, 17)]*5: + for shape in [(100, 100), (5, 8, 13, 17)] * 5: yield _test_random, shape + def _test_random(shape): # Just tests for crashing -- not for correctness. np.random.seed(0) a = np.random.random(shape).astype(np.float32) - starts = [[0]*len(shape), [-1]*len(shape), - (np.random.random(len(shape))*shape).astype(int)] - ends = [(np.random.random(len(shape))*shape).astype(int) for i in range(4)] + starts = [[0] * len(shape), [-1] * len(shape), + (np.random.random(len(shape)) * shape).astype(int)] + ends = [(np.random.random(len(shape)) * shape).astype(int) + for i in range(4)] m = mcp.MCP(a, fully_connected=True) costs, offsets = m.find_costs(starts) - for point in [(np.random.random(len(shape))*shape).astype(int) + for point in [(np.random.random(len(shape)) * shape).astype(int) for i in range(4)]: m.traceback(point) m._reset() diff --git a/skimage/graph/tests/test_spath.py b/skimage/graph/tests/test_spath.py index a9f6b274..62f9f303 100644 --- a/skimage/graph/tests/test_spath.py +++ b/skimage/graph/tests/test_spath.py @@ -3,6 +3,7 @@ from numpy.testing import * import skimage.graph.spath as spath + def test_basic(): x = np.array([[1, 1, 3], [0, 2, 0], @@ -11,6 +12,7 @@ def test_basic(): assert_array_equal(path, [0, 0, 1]) assert_equal(cost, 1) + def test_reach(): x = np.array([[1, 1, 3], [0, 2, 0], @@ -19,6 +21,7 @@ def test_reach(): assert_array_equal(path, [0, 0, 2]) assert_equal(cost, 0) + def test_non_square(): x = np.array([[1, 1, 1, 1, 5, 5, 5], [5, 0, 0, 5, 9, 1, 1], diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 05a7a8b7..477fe8bc 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -8,6 +8,7 @@ import numpy as np # Shared image queue _image_stack = [] + def push(img): """Push an image onto the shared image stack. @@ -22,6 +23,7 @@ def push(img): _image_stack.append(img) + def pop(): """Pop an image from the shared image stack. @@ -33,6 +35,7 @@ def pop(): """ return _image_stack.pop() + def imread(fname, as_grey=False, plugin=None, flatten=None, **plugin_args): """Load an image from file. @@ -76,6 +79,7 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, return img + def imread_collection(load_pattern, conserve_memory=True, plugin=None, **plugin_args): """ @@ -128,6 +132,7 @@ def imsave(fname, arr, plugin=None, **plugin_args): """ return call_plugin('imsave', fname, arr, plugin=plugin, **plugin_args) + def imshow(arr, plugin=None, **plugin_args): """Display an image. @@ -148,6 +153,7 @@ def imshow(arr, plugin=None, **plugin_args): """ return call_plugin('imshow', arr, plugin=plugin, **plugin_args) + def show(): '''Display pending images. diff --git a/skimage/io/_plugins/fits_plugin.py b/skimage/io/_plugins/fits_plugin.py index b6627032..9eb8e028 100644 --- a/skimage/io/_plugins/fits_plugin.py +++ b/skimage/io/_plugins/fits_plugin.py @@ -49,9 +49,9 @@ def imread(fname, dtype=None): for hdu in hdulist: if isinstance(hdu, pyfits.ImageHDU) or \ isinstance(hdu, pyfits.PrimaryHDU): - if hdu.data is not None: - img_array = hdu.data - break + if hdu.data is not None: + img_array = hdu.data + break hdulist.close() return img_array @@ -109,7 +109,7 @@ def imread_collection(load_pattern, conserve_memory=True): def FITSFactory(image_ext): """Load an image extension from a FITS file and return a NumPy array - + Parameters ---------- @@ -136,7 +136,7 @@ def FITSFactory(image_ext): raise ValueError("Expected a (filename, extension) tuple") hdulist = pyfits.open(filename) - + data = hdulist[extnum].data hdulist.close() @@ -146,4 +146,3 @@ def FITSFactory(image_ext): (extnum, filename)) return data - diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index 5d023909..a43ff749 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -20,7 +20,7 @@ def _generate_candidate_libs(): lib_dirs.append(os.path.join(os.environ['HOME'], 'lib')) lib_dirs = [ld for ld in lib_dirs if os.path.exists(ld)] - lib_names = ['libfreeimage', 'freeimage'] # should be lower-case! + lib_names = ['libfreeimage', 'freeimage'] # should be lower-case! # Now attempt to find libraries of that name in the given directory # (case-insensitive and without regard for extension) lib_paths = [] @@ -34,6 +34,7 @@ def _generate_candidate_libs(): return lib_dirs, lib_paths + def load_freeimage(): if sys.platform == 'win32': loader = ctypes.windll @@ -70,13 +71,13 @@ def load_freeimage(): if errors: # No freeimage library loaded, and load-errors reported for some # candidate libs - err_txt = ['%s:\n%s'%(l, str(e.message)) for l, e in errors] + err_txt = ['%s:\n%s' % (l, str(e.message)) for l, e in errors] raise OSError('One or more FreeImage libraries were found, but ' - 'could not be loaded due to the following errors:\n'+ + 'could not be loaded due to the following errors:\n' + '\n\n'.join(err_txt)) else: # No errors, because no potential libraries found at all! - raise OSError('Could not find a FreeImage library in any of:\n'+ + raise OSError('Could not find a FreeImage library in any of:\n' + '\n'.join(lib_dirs)) # FreeImage found @@ -113,7 +114,9 @@ API = { } # Albert's ctypes pattern -def register_api(lib,api): + + +def register_api(lib, api): for f, (restype, argtypes) in api.items(): func = getattr(lib, f) func.restype = restype @@ -205,111 +208,112 @@ class FI_TYPES(object): extra_dims = cls.extra_dims[fi_type] return numpy.dtype(dtype), extra_dims + [w, h] + class IO_FLAGS(object): - FIF_LOAD_NOPIXELS = 0x8000 # loading: load the image header only + FIF_LOAD_NOPIXELS = 0x8000 # loading: load the image header only # (not supported by all plugins) BMP_DEFAULT = 0 BMP_SAVE_RLE = 1 CUT_DEFAULT = 0 DDS_DEFAULT = 0 - EXR_DEFAULT = 0 # save data as half with piz-based wavelet compression - EXR_FLOAT = 0x0001 # save data as float instead of as half (not recommended) - EXR_NONE = 0x0002 # save with no compression - EXR_ZIP = 0x0004 # save with zlib compression, in blocks of 16 scan lines - EXR_PIZ = 0x0008 # save with piz-based wavelet compression - EXR_PXR24 = 0x0010 # save with lossy 24-bit float compression - EXR_B44 = 0x0020 # save with lossy 44% float compression + EXR_DEFAULT = 0 # save data as half with piz-based wavelet compression + EXR_FLOAT = 0x0001 # save data as float instead of as half (not recommended) + EXR_NONE = 0x0002 # save with no compression + EXR_ZIP = 0x0004 # save with zlib compression, in blocks of 16 scan lines + EXR_PIZ = 0x0008 # save with piz-based wavelet compression + EXR_PXR24 = 0x0010 # save with lossy 24-bit float compression + EXR_B44 = 0x0020 # save with lossy 44% float compression # - goes to 22% when combined with EXR_LC - EXR_LC = 0x0040 # save images with one luminance and two chroma channels, + EXR_LC = 0x0040 # save images with one luminance and two chroma channels, # rather than as RGB (lossy compression) FAXG3_DEFAULT = 0 GIF_DEFAULT = 0 - GIF_LOAD256 = 1 # Load the image as a 256 color image with ununsed + GIF_LOAD256 = 1 # Load the image as a 256 color image with ununsed # palette entries, if it's 16 or 2 color - GIF_PLAYBACK = 2 # 'Play' the GIF to generate each frame (as 32bpp) + GIF_PLAYBACK = 2 # 'Play' the GIF to generate each frame (as 32bpp) # instead of returning raw frame data when loading HDR_DEFAULT = 0 ICO_DEFAULT = 0 - ICO_MAKEALPHA = 1 # convert to 32bpp and create an alpha channel from the + ICO_MAKEALPHA = 1 # convert to 32bpp and create an alpha channel from the # AND-mask when loading IFF_DEFAULT = 0 - J2K_DEFAULT = 0 # save with a 16:1 rate - JP2_DEFAULT = 0 # save with a 16:1 rate - JPEG_DEFAULT = 0 # loading (see JPEG_FAST); + J2K_DEFAULT = 0 # save with a 16:1 rate + JP2_DEFAULT = 0 # save with a 16:1 rate + JPEG_DEFAULT = 0 # loading (see JPEG_FAST); # saving (see JPEG_QUALITYGOOD|JPEG_SUBSAMPLING_420) - JPEG_FAST = 0x0001 # load the file as fast as possible, + JPEG_FAST = 0x0001 # load the file as fast as possible, # sacrificing some quality - JPEG_ACCURATE = 0x0002 # load the file with the best quality, + JPEG_ACCURATE = 0x0002 # load the file with the best quality, # sacrificing some speed - JPEG_CMYK = 0x0004 # load separated CMYK "as is" + JPEG_CMYK = 0x0004 # load separated CMYK "as is" # (use | to combine with other load flags) - JPEG_EXIFROTATE = 0x0008 # load and rotate according to + JPEG_EXIFROTATE = 0x0008 # load and rotate according to # Exif 'Orientation' tag if available - JPEG_QUALITYSUPERB = 0x80 # save with superb quality (100:1) - JPEG_QUALITYGOOD = 0x0100 # save with good quality (75:1) - JPEG_QUALITYNORMAL = 0x0200 # save with normal quality (50:1) - JPEG_QUALITYAVERAGE = 0x0400 # save with average quality (25:1) - JPEG_QUALITYBAD = 0x0800 # save with bad quality (10:1) - JPEG_PROGRESSIVE = 0x2000 # save as a progressive-JPEG + JPEG_QUALITYSUPERB = 0x80 # save with superb quality (100:1) + JPEG_QUALITYGOOD = 0x0100 # save with good quality (75:1) + JPEG_QUALITYNORMAL = 0x0200 # save with normal quality (50:1) + JPEG_QUALITYAVERAGE = 0x0400 # save with average quality (25:1) + JPEG_QUALITYBAD = 0x0800 # save with bad quality (10:1) + JPEG_PROGRESSIVE = 0x2000 # save as a progressive-JPEG # (use | to combine with other save flags) - JPEG_SUBSAMPLING_411 = 0x1000 # save with high 4x1 chroma + JPEG_SUBSAMPLING_411 = 0x1000 # save with high 4x1 chroma # subsampling (4:1:1) - JPEG_SUBSAMPLING_420 = 0x4000 # save with medium 2x2 medium chroma + JPEG_SUBSAMPLING_420 = 0x4000 # save with medium 2x2 medium chroma # subsampling (4:2:0) - default value - JPEG_SUBSAMPLING_422 = 0x8000 # save with low 2x1 chroma subsampling (4:2:2) - JPEG_SUBSAMPLING_444 = 0x10000 # save with no chroma subsampling (4:4:4) - JPEG_OPTIMIZE = 0x20000 # on saving, compute optimal Huffman coding tables + JPEG_SUBSAMPLING_422 = 0x8000 # save with low 2x1 chroma subsampling (4:2:2) + JPEG_SUBSAMPLING_444 = 0x10000 # save with no chroma subsampling (4:4:4) + JPEG_OPTIMIZE = 0x20000 # on saving, compute optimal Huffman coding tables # (can reduce a few percent of file size) - JPEG_BASELINE = 0x40000 # save basic JPEG, without metadata or any markers + JPEG_BASELINE = 0x40000 # save basic JPEG, without metadata or any markers KOALA_DEFAULT = 0 LBM_DEFAULT = 0 MNG_DEFAULT = 0 PCD_DEFAULT = 0 - PCD_BASE = 1 # load the bitmap sized 768 x 512 - PCD_BASEDIV4 = 2 # load the bitmap sized 384 x 256 - PCD_BASEDIV16 = 3 # load the bitmap sized 192 x 128 + PCD_BASE = 1 # load the bitmap sized 768 x 512 + PCD_BASEDIV4 = 2 # load the bitmap sized 384 x 256 + PCD_BASEDIV16 = 3 # load the bitmap sized 192 x 128 PCX_DEFAULT = 0 PFM_DEFAULT = 0 PICT_DEFAULT = 0 PNG_DEFAULT = 0 - PNG_IGNOREGAMMA = 1 # loading: avoid gamma correction - PNG_Z_BEST_SPEED = 0x0001 # save using ZLib level 1 compression flag + PNG_IGNOREGAMMA = 1 # loading: avoid gamma correction + PNG_Z_BEST_SPEED = 0x0001 # save using ZLib level 1 compression flag # (default value is 6) - PNG_Z_DEFAULT_COMPRESSION = 0x0006 # save using ZLib level 6 compression + PNG_Z_DEFAULT_COMPRESSION = 0x0006 # save using ZLib level 6 compression # flag (default recommended value) - PNG_Z_BEST_COMPRESSION = 0x0009 # save using ZLib level 9 compression flag + PNG_Z_BEST_COMPRESSION = 0x0009 # save using ZLib level 9 compression flag # (default value is 6) - PNG_Z_NO_COMPRESSION = 0x0100 # save without ZLib compression - PNG_INTERLACED = 0x0200 # save using Adam7 interlacing (use | to combine + PNG_Z_NO_COMPRESSION = 0x0100 # save without ZLib compression + PNG_INTERLACED = 0x0200 # save using Adam7 interlacing (use | to combine # with other save flags) PNM_DEFAULT = 0 - PNM_SAVE_RAW = 0 # Writer saves in RAW format (i.e. P4, P5 or P6) - PNM_SAVE_ASCII = 1 # Writer saves in ASCII format (i.e. P1, P2 or P3) + PNM_SAVE_RAW = 0 # Writer saves in RAW format (i.e. P4, P5 or P6) + PNM_SAVE_ASCII = 1 # Writer saves in ASCII format (i.e. P1, P2 or P3) PSD_DEFAULT = 0 - PSD_CMYK = 1 # reads tags for separated CMYK (default is conversion to RGB) - PSD_LAB = 2 # reads tags for CIELab (default is conversion to RGB) + PSD_CMYK = 1 # reads tags for separated CMYK (default is conversion to RGB) + PSD_LAB = 2 # reads tags for CIELab (default is conversion to RGB) RAS_DEFAULT = 0 - RAW_DEFAULT = 0 # load the file as linear RGB 48-bit - RAW_PREVIEW = 1 # try to load the embedded JPEG preview with included + RAW_DEFAULT = 0 # load the file as linear RGB 48-bit + RAW_PREVIEW = 1 # try to load the embedded JPEG preview with included # Exif Data or default to RGB 24-bit - RAW_DISPLAY = 2 # load the file as RGB 24-bit + RAW_DISPLAY = 2 # load the file as RGB 24-bit SGI_DEFAULT = 0 TARGA_DEFAULT = 0 - TARGA_LOAD_RGB888 = 1 # Convert RGB555 and ARGB8888 -> RGB888. - TARGA_SAVE_RLE = 2 # Save with RLE compression + TARGA_LOAD_RGB888 = 1 # Convert RGB555 and ARGB8888 -> RGB888. + TARGA_SAVE_RLE = 2 # Save with RLE compression TIFF_DEFAULT = 0 - TIFF_CMYK = 0x0001 # reads/stores tags for separated CMYK + TIFF_CMYK = 0x0001 # reads/stores tags for separated CMYK # (use | to combine with compression flags) - TIFF_PACKBITS = 0x0100 # save using PACKBITS compression - TIFF_DEFLATE = 0x0200 # save using DEFLATE (a.k.a. ZLIB) compression - TIFF_ADOBE_DEFLATE = 0x0400 # save using ADOBE DEFLATE compression - TIFF_NONE = 0x0800 # save without any compression - TIFF_CCITTFAX3 = 0x1000 # save using CCITT Group 3 fax encoding - TIFF_CCITTFAX4 = 0x2000 # save using CCITT Group 4 fax encoding - TIFF_LZW = 0x4000 # save using LZW compression - TIFF_JPEG = 0x8000 # save using JPEG compression - TIFF_LOGLUV = 0x10000 # save using LogLuv compression + TIFF_PACKBITS = 0x0100 # save using PACKBITS compression + TIFF_DEFLATE = 0x0200 # save using DEFLATE (a.k.a. ZLIB) compression + TIFF_ADOBE_DEFLATE = 0x0400 # save using ADOBE DEFLATE compression + TIFF_NONE = 0x0800 # save without any compression + TIFF_CCITTFAX3 = 0x1000 # save using CCITT Group 3 fax encoding + TIFF_CCITTFAX4 = 0x2000 # save using CCITT Group 4 fax encoding + TIFF_LZW = 0x4000 # save using LZW compression + TIFF_JPEG = 0x8000 # save using JPEG compression + TIFF_LOGLUV = 0x10000 # save using LogLuv compression WBMP_DEFAULT = 0 XBM_DEFAULT = 0 XPM_DEFAULT = 0 @@ -329,23 +333,23 @@ class METADATA_MODELS(object): class METADATA_DATATYPE(object): - FIDT_BYTE = 1 # 8-bit unsigned integer - FIDT_ASCII = 2 # 8-bit bytes w/ last byte null - FIDT_SHORT = 3 # 16-bit unsigned integer - FIDT_LONG = 4 # 32-bit unsigned integer - FIDT_RATIONAL = 5 # 64-bit unsigned fraction - FIDT_SBYTE = 6 # 8-bit signed integer - FIDT_UNDEFINED = 7 # 8-bit untyped data - FIDT_SSHORT = 8 # 16-bit signed integer - FIDT_SLONG = 9 # 32-bit signed integer - FIDT_SRATIONAL = 10 # 64-bit signed fraction - FIDT_FLOAT = 11 # 32-bit IEEE floating point - FIDT_DOUBLE = 12 # 64-bit IEEE floating point - FIDT_IFD = 13 # 32-bit unsigned integer (offset) - FIDT_PALETTE = 14 # 32-bit RGBQUAD - FIDT_LONG8 = 16 # 64-bit unsigned integer - FIDT_SLONG8 = 17 # 64-bit signed integer - FIDT_IFD8 = 18 # 64-bit unsigned integer (offset) + FIDT_BYTE = 1 # 8-bit unsigned integer + FIDT_ASCII = 2 # 8-bit bytes w/ last byte null + FIDT_SHORT = 3 # 16-bit unsigned integer + FIDT_LONG = 4 # 32-bit unsigned integer + FIDT_RATIONAL = 5 # 64-bit unsigned fraction + FIDT_SBYTE = 6 # 8-bit signed integer + FIDT_UNDEFINED = 7 # 8-bit untyped data + FIDT_SSHORT = 8 # 16-bit signed integer + FIDT_SLONG = 9 # 32-bit signed integer + FIDT_SRATIONAL = 10 # 64-bit signed fraction + FIDT_FLOAT = 11 # 32-bit IEEE floating point + FIDT_DOUBLE = 12 # 64-bit IEEE floating point + FIDT_IFD = 13 # 32-bit unsigned integer (offset) + FIDT_PALETTE = 14 # 32-bit RGBQUAD + FIDT_LONG8 = 16 # 64-bit unsigned integer + FIDT_SLONG8 = 17 # 64-bit signed integer + FIDT_IFD8 = 18 # 64-bit unsigned integer (offset) dtypes = { FIDT_BYTE: numpy.uint8, @@ -384,6 +388,7 @@ def _process_bitmap(filename, flags, process_func): finally: _FI.FreeImage_Unload(bitmap) + def read(filename, flags=0): """Read an image to a numpy array of shape (height, width) for greyscale images, or shape (height, width, nchannels) for RGB or @@ -394,6 +399,7 @@ def read(filename, flags=0): """ return _process_bitmap(filename, flags, _array_from_bitmap) + def read_metadata(filename): """Return a dict containing all image metadata. @@ -404,6 +410,7 @@ def read_metadata(filename): flags = IO_FLAGS.FIF_LOAD_NOPIXELS return _process_bitmap(filename, flags, _read_metadata) + def _process_multipage(filename, flags, process_func): filename = asbytes(filename) ftype = _FI.FreeImage_GetFileType(filename, 0) @@ -435,6 +442,7 @@ def _process_multipage(filename, flags, process_func): finally: _FI.FreeImage_CloseMultiBitmap(multibitmap, 0) + def read_multipage(filename, flags=0): """Read a multipage image to a list of numpy arrays, where each array is of shape (height, width) for greyscale images, or shape @@ -445,6 +453,7 @@ def read_multipage(filename, flags=0): """ return _process_multipage(filename, flags, _array_from_bitmap) + def read_multipage_metadata(filename): """Read a multipage image to a list of metadata dicts, one dict for each page. The dict format is as in read_metadata(). @@ -452,26 +461,28 @@ def read_multipage_metadata(filename): flags = IO_FLAGS.FIF_LOAD_NOPIXELS return _process_multipage(filename, flags, _read_metadata) + def _wrap_bitmap_bits_in_array(bitmap, shape, dtype): - """Return an ndarray view on the data in a FreeImage bitmap. Only - valid for as long as the bitmap is loaded (if single page) / locked - in memory (if multipage). + """Return an ndarray view on the data in a FreeImage bitmap. Only + valid for as long as the bitmap is loaded (if single page) / locked + in memory (if multipage). - """ - pitch = _FI.FreeImage_GetPitch(bitmap) - height = shape[-1] - byte_size = height * pitch - itemsize = dtype.itemsize + """ + pitch = _FI.FreeImage_GetPitch(bitmap) + height = shape[-1] + byte_size = height * pitch + itemsize = dtype.itemsize + + if len(shape) == 3: + strides = (itemsize, shape[0] * itemsize, pitch) + else: + strides = (itemsize, pitch) + bits = _FI.FreeImage_GetBits(bitmap) + array = numpy.ndarray(shape, dtype=dtype, + buffer=(ctypes.c_char * byte_size).from_address(bits), + strides=strides) + return array - if len(shape) == 3: - strides = (itemsize, shape[0]*itemsize, pitch) - else: - strides = (itemsize, pitch) - bits = _FI.FreeImage_GetBits(bitmap) - array = numpy.ndarray(shape, dtype=dtype, - buffer=(ctypes.c_char*byte_size).from_address(bits), - strides=strides) - return array def _array_from_bitmap(bitmap): """Convert a FreeImage bitmap pointer to a numpy array. @@ -482,6 +493,7 @@ def _array_from_bitmap(bitmap): # swizzle the color components and flip the scanlines to go from # FreeImage's BGR[A] and upside-down internal memory format to something # more normal + def n(arr): return arr[..., ::-1].T if len(shape) == 3 and _FI.FreeImage_IsLittleEndian() and \ @@ -490,10 +502,10 @@ def _array_from_bitmap(bitmap): g = n(array[1]) r = n(array[2]) if shape[0] == 3: - return numpy.dstack( (r, g, b) ) + return numpy.dstack((r, g, b)) elif shape[0] == 4: a = n(array[3]) - return numpy.dstack( (r, g, b, a) ) + return numpy.dstack((r, g, b, a)) else: raise ValueError('Cannot handle images of shape %s' % shape) @@ -501,6 +513,7 @@ def _array_from_bitmap(bitmap): # after bitmap is freed. return n(array).copy() + def _read_metadata(bitmap): metadata = {} models = [(name[5:], number) for name, number in @@ -531,6 +544,7 @@ def _read_metadata(bitmap): _FI.FreeImage_FindCloseMetadata(mdhandle) return metadata + def write(array, filename, flags=0): """Write a (height, width) or (height, width, nchannels) array to a greyscale, RGB, or RGBA image, with file type deduced from the @@ -558,7 +572,8 @@ def write(array, filename, flags=0): if not res: raise RuntimeError('Could not save image properly.') finally: - _FI.FreeImage_Unload(bitmap) + _FI.FreeImage_Unload(bitmap) + def write_multipage(arrays, filename, flags=0): """Write a list of (height, width) or (height, width, nchannels) @@ -592,19 +607,20 @@ def write_multipage(arrays, filename, flags=0): # 4-byte quads of 0,v,v,v from 0,0,0,0 to 0,255,255,255 _GREY_PALETTE = numpy.arange(0, 0x01000000, 0x00010101, dtype=numpy.uint32) + def _array_to_bitmap(array): """Allocate a FreeImage bitmap and copy a numpy array into it. """ shape = array.shape dtype = array.dtype - r,c = shape[:2] + r, c = shape[:2] if len(shape) == 2: n_channels = 1 - w_shape = (c,r) + w_shape = (c, r) elif len(shape) == 3: n_channels = shape[2] - w_shape = (n_channels,c,r) + w_shape = (n_channels, c, r) else: n_channels = shape[0] try: @@ -619,18 +635,18 @@ def _array_to_bitmap(array): if not bitmap: raise RuntimeError('Could not allocate image for storage') try: - def n(arr): # normalise to freeimage's in-memory format - return arr.T[:,::-1] + def n(arr): # normalise to freeimage's in-memory format + return arr.T[:, ::-1] wrapped_array = _wrap_bitmap_bits_in_array(bitmap, w_shape, dtype) # swizzle the color components and flip the scanlines to go to # FreeImage's BGR[A] and upside-down internal memory format if len(shape) == 3 and _FI.FreeImage_IsLittleEndian() and \ dtype.type == numpy.uint8: - wrapped_array[0] = n(array[:,:,2]) - wrapped_array[1] = n(array[:,:,1]) - wrapped_array[2] = n(array[:,:,0]) + wrapped_array[0] = n(array[:, :, 2]) + wrapped_array[1] = n(array[:, :, 1]) + wrapped_array[2] = n(array[:, :, 0]) if shape[2] == 4: - wrapped_array[3] = n(array[:,:,3]) + wrapped_array[3] = n(array[:, :, 3]) else: wrapped_array[:] = n(array) if len(shape) == 2 and dtype.type == numpy.uint8: @@ -641,8 +657,8 @@ def _array_to_bitmap(array): ctypes.memmove(palette, _GREY_PALETTE.ctypes.data, 1024) return bitmap, fi_type except: - _FI.FreeImage_Unload(bitmap) - raise + _FI.FreeImage_Unload(bitmap) + raise def imread(filename): @@ -661,6 +677,7 @@ def imread(filename): img = read(filename) return img + def imsave(filename, img): ''' imsave(filename, img) diff --git a/skimage/io/_plugins/gdal_plugin.py b/skimage/io/_plugins/gdal_plugin.py index acc6db36..f4c2a1ae 100644 --- a/skimage/io/_plugins/gdal_plugin.py +++ b/skimage/io/_plugins/gdal_plugin.py @@ -9,6 +9,7 @@ except ImportError: "Please refer to http://www.gdal.org/ " "for further instructions.") + def imread(fname, dtype=None): """Load an image from file. @@ -16,4 +17,3 @@ def imread(fname, dtype=None): ds = gdal.Open(fname) return ds.ReadAsArray().astype(dtype) - diff --git a/skimage/io/_plugins/matplotlib_plugin.py b/skimage/io/_plugins/matplotlib_plugin.py index 652c8be1..ece44d93 100644 --- a/skimage/io/_plugins/matplotlib_plugin.py +++ b/skimage/io/_plugins/matplotlib_plugin.py @@ -1,5 +1,6 @@ import matplotlib.pyplot as plt + def imshow(*args, **kwargs): kwargs.setdefault('interpolation', 'nearest') kwargs.setdefault('cmap', 'gray') @@ -8,5 +9,6 @@ def imshow(*args, **kwargs): imread = plt.imread show = plt.show + def _app_show(): show() diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 914f7e9f..96106c5c 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -11,6 +11,7 @@ except ImportError: from skimage.util import img_as_ubyte + def imread(fname, dtype=None): """Load an image from file. @@ -33,6 +34,7 @@ def imread(fname, dtype=None): return np.array(im, dtype=dtype) + def _palette_is_grayscale(pil_image): """Return True if PIL image in palette mode is grayscale. @@ -56,6 +58,7 @@ def _palette_is_grayscale(pil_image): # are all zero. return np.allclose(np.diff(valid_palette), 0) + def imsave(fname, arr): """Save an image to disk. @@ -100,6 +103,7 @@ def imsave(fname, arr): img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) img.save(fname) + def imshow(arr): """Display an image, using PIL's default display command. @@ -112,5 +116,6 @@ def imshow(arr): """ Image.fromarray(img_as_ubyte(arr)).show() + def _app_show(): pass diff --git a/skimage/io/_plugins/plugin.py b/skimage/io/_plugins/plugin.py index 66ac3911..da69363d 100644 --- a/skimage/io/_plugins/plugin.py +++ b/skimage/io/_plugins/plugin.py @@ -4,7 +4,6 @@ __all__ = ['use', 'available', 'call', 'info', 'configuration', 'reset_plugins'] -import warnings from ConfigParser import ConfigParser import os.path from glob import glob @@ -15,8 +14,9 @@ plugin_provides = {} plugin_module_name = {} plugin_meta_data = {} + def reset_plugins(): - """Clear the plugin state to the default, i.e., where no plugins are loaded. + """Clear the plugin state to the default, i.e., where no plugins are loaded """ global plugin_store @@ -28,6 +28,7 @@ def reset_plugins(): reset_plugins() + def _scan_plugins(): """Scan the plugins directory for .ini files and parse them to gather plugin meta-data. @@ -59,6 +60,7 @@ def _scan_plugins(): _scan_plugins() + def call(kind, *args, **kwargs): """Find the appropriate plugin of 'kind' and execute it. @@ -90,13 +92,14 @@ command. A list of all available plugins can be found using else: _load(plugin) try: - func = [f for (p,f) in plugin_funcs if p == plugin][0] + func = [f for (p, f) in plugin_funcs if p == plugin][0] except IndexError: raise RuntimeError('Could not find the plugin "%s" for %s.' % \ (plugin, kind)) return func(*args, **kwargs) + def use(name, kind=None): """Set the default plugin for a specified operation. The plugin will be loaded if it hasn't been already. @@ -149,6 +152,7 @@ def use(name, kind=None): plugin_store[k] = funcs + def available(loaded=False): """List available plugins. @@ -178,6 +182,7 @@ def available(loaded=False): return d + def _load(plugin): """Load the given plugin. @@ -211,6 +216,7 @@ def _load(plugin): if not (plugin, func) in store: store.append((plugin, func)) + def info(plugin): """Return plugin meta-data. @@ -230,6 +236,7 @@ def info(plugin): except KeyError: raise ValueError('No information on plugin "%s"' % plugin) + def configuration(): """Return the currently preferred plugin order. diff --git a/skimage/io/_plugins/q_color_mixer.py b/skimage/io/_plugins/q_color_mixer.py index 5a60b92b..5b6a461a 100644 --- a/skimage/io/_plugins/q_color_mixer.py +++ b/skimage/io/_plugins/q_color_mixer.py @@ -36,7 +36,8 @@ class IntelligentSlider(QWidget): self.name_label.setAlignment(QtCore.Qt.AlignCenter) self.value_label = QLabel() - self.value_label.setText('%2.2f' % (self.slider.value() * self.a + self.b)) + self.value_label.setText( + '%2.2f' % (self.slider.value() * self.a + self.b)) self.value_label.setAlignment(QtCore.Qt.AlignCenter) self.layout = QGridLayout(self) @@ -120,11 +121,9 @@ class MixerPanel(QtGui.QFrame): self.rgb_widget.layout.addWidget(self.gs, 2, 1) self.rgb_widget.layout.addWidget(self.bs, 2, 2) - #--------------------------------------------------------------- # HSV sliders #--------------------------------------------------------------- - # radio buttons self.hsv_add = QtGui.QRadioButton('Additive') self.hsv_mul = QtGui.QRadioButton('Multiplicative') @@ -147,11 +146,9 @@ class MixerPanel(QtGui.QFrame): self.hsv_widget.layout.addWidget(self.ss, 2, 1) self.hsv_widget.layout.addWidget(self.vs, 2, 2) - #--------------------------------------------------------------- # Brightness/Contrast sliders #--------------------------------------------------------------- - # sliders cont = IntelligentSlider('x', 0.002, 0, self.bright_changed) bright = IntelligentSlider('+', 0.51, -255, self.bright_changed) @@ -164,10 +161,9 @@ class MixerPanel(QtGui.QFrame): self.bright_widget.layout.addWidget(self.cont, 0, 0) self.bright_widget.layout.addWidget(self.bright, 0, 1) - - #----------------------------------------------------------------------- + #---------------------------------------------------------------------- # Gamma Slider - #----------------------------------------------------------------------- + #---------------------------------------------------------------------- gamma = IntelligentSlider('gamma', 0.005, 0, self.gamma_changed) self.gamma = gamma @@ -176,11 +172,9 @@ class MixerPanel(QtGui.QFrame): self.gamma_widget.layout = QtGui.QGridLayout(self.gamma_widget) self.gamma_widget.layout.addWidget(self.gamma, 0, 0) - #--------------------------------------------------------------- # Sigmoid Gamma sliders #--------------------------------------------------------------- - # sliders alpha = IntelligentSlider('alpha', 0.011, 1, self.sig_gamma_changed) beta = IntelligentSlider('beta', 0.012, 0, self.sig_gamma_changed) @@ -225,7 +219,6 @@ class MixerPanel(QtGui.QFrame): self.rgb_mul.setChecked(True) self.hsv_mul.setChecked(True) - def set_callback(self, callback): self.callback = callback @@ -281,7 +274,6 @@ class MixerPanel(QtGui.QFrame): self.a_gamma.set_value(1) self.b_gamma.set_value(0.5) - def rgb_changed(self, name, val): if name == 'R': channel = self.mixer.RED @@ -346,4 +338,4 @@ class MixerPanel(QtGui.QFrame): self.reset_sliders() if self.callback: - self.callback() \ No newline at end of file + self.callback() diff --git a/skimage/io/_plugins/q_histogram.py b/skimage/io/_plugins/q_histogram.py index 36edb30d..0a60ce9f 100644 --- a/skimage/io/_plugins/q_histogram.py +++ b/skimage/io/_plugins/q_histogram.py @@ -39,7 +39,7 @@ class ColorHistogram(QWidget): orig_height = self.height() # fill perc % of the widget - perc = 1 + perc = 1 width = int(orig_width * perc) height = int(orig_height * perc) @@ -60,14 +60,14 @@ class ColorHistogram(QWidget): remainder = width % nbars bar_width = [int(width / nbars)] * nbars for i in range(remainder): - bar_width[i]+=1 + bar_width[i] += 1 paint = QPainter() paint.begin(self) # determine the scaling factor max_val = np.max(self.counts) - scale = 1. * height / max_val + scale = 1. * height / max_val # determine if we have a colormap and drop into the appopriate # loop. @@ -95,7 +95,6 @@ class ColorHistogram(QWidget): paint.end() - def update_hist(self, counts, cmap): self._validate_input(counts, cmap) self.counts = counts @@ -121,17 +120,17 @@ class QuadHistogram(QFrame): self.b_hist = ColorHistogram(b, (0, 0, 255)) self.v_hist = ColorHistogram(v, (0, 0, 0)) - self.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken) + self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.layout = QGridLayout(self) self.layout.setMargin(0) order_map = {'R': self.r_hist, 'G': self.g_hist, 'B': self.b_hist, 'V': self.v_hist} - if layout=='vertical': + if layout == 'vertical': for i in range(len(order)): self.layout.addWidget(order_map[order[i]], i, 0) - elif layout=='horizontal': + elif layout == 'horizontal': for i in range(len(order)): self.layout.addWidget(order_map[order[i]], 0, i) @@ -140,4 +139,4 @@ class QuadHistogram(QFrame): self.r_hist.update_hist(r, (255, 0, 0)) self.g_hist.update_hist(g, (0, 255, 0)) self.b_hist.update_hist(b, (0, 0, 255)) - self.v_hist.update_hist(v, (0, 0, 0)) \ No newline at end of file + self.v_hist.update_hist(v, (0, 0, 0)) diff --git a/skimage/io/_plugins/skivi.py b/skimage/io/_plugins/skivi.py index 440e3c2c..241bdf94 100644 --- a/skimage/io/_plugins/skivi.py +++ b/skimage/io/_plugins/skivi.py @@ -69,7 +69,7 @@ class ImageLabel(QLabel): class RGBHSVDisplay(QFrame): def __init__(self): QFrame.__init__(self) - self.setFrameStyle(QtGui.QFrame.Box|QtGui.QFrame.Sunken) + self.setFrameStyle(QtGui.QFrame.Box | QtGui.QFrame.Sunken) self.posx_label = QLabel('X-pos:') self.posx_value = QLabel() @@ -118,7 +118,6 @@ class RGBHSVDisplay(QFrame): self.v_value.setText(str(v)[:5]) - class SkiviImageWindow(QMainWindow): def __init__(self, arr, mgr): QMainWindow.__init__(self) @@ -132,7 +131,8 @@ class SkiviImageWindow(QMainWindow): self.label = ImageLabel(self, arr) self.label_container = QFrame() - self.label_container.setFrameShape(QtGui.QFrame.StyledPanel|QtGui.QFrame.Sunken) + self.label_container.setFrameShape( + QtGui.QFrame.StyledPanel | QtGui.QFrame.Sunken) self.label_container.setLineWidth(1) self.label_container.layout = QtGui.QGridLayout(self.label_container) @@ -171,7 +171,6 @@ class SkiviImageWindow(QMainWindow): self.layout.addWidget(self.save_stack, 1, 1) self.layout.addWidget(self.save_file, 1, 2) - def closeEvent(self, event): # Allow window to be destroyed by removing any # references to it @@ -206,14 +205,13 @@ class SkiviImageWindow(QMainWindow): if x >= maxw or y >= maxh or x < 0 or y < 0: r = g = b = h = s = v = '' else: - r = self.arr[y,x,0] - g = self.arr[y,x,1] - b = self.arr[y,x,2] + r = self.arr[y, x, 0] + g = self.arr[y, x, 1] + b = self.arr[y, x, 2] h, s, v = self.mixer_panel.mixer.rgb_2_hsv_pixel(r, g, b) self.rgb_hsv_disp.update_vals((x, y, r, g, b, h, s, v)) - def save_to_stack(self): from skimage import io img = self.arr.copy() @@ -238,5 +236,3 @@ class SkiviImageWindow(QMainWindow): if len(filename) == 0: return io.imsave(filename, self.arr) - - diff --git a/skimage/io/_plugins/test_plugin.py b/skimage/io/_plugins/test_plugin.py index 2956f14f..18f750f3 100644 --- a/skimage/io/_plugins/test_plugin.py +++ b/skimage/io/_plugins/test_plugin.py @@ -1,18 +1,22 @@ # This mock-up is called by ../tests/test_plugin.py # to verify the behaviour of the plugin infrastructure + def imread(fname, dtype=None): assert fname == 'test.png' assert dtype == 'i4' + def imsave(fname, arr): assert fname == 'test.png' assert arr == [1, 2, 3] + def imshow(arr, plugin_arg=None): assert arr == [1, 2, 3] assert plugin_arg == (1, 2) + def imread_collection(x, conserve_memory=True): assert conserve_memory == False assert x == '*.png' diff --git a/skimage/io/_plugins/util.py b/skimage/io/_plugins/util.py index 7eb109b7..f40a04a8 100644 --- a/skimage/io/_plugins/util.py +++ b/skimage/io/_plugins/util.py @@ -12,6 +12,7 @@ try: except: CPU_COUNT = 2 + class GuiLockError(Exception): def __init__(self, msg): self.msg = msg @@ -19,6 +20,7 @@ class GuiLockError(Exception): def __str__(self): return self.msg + class WindowManager(object): ''' A class to keep track of spawned windows, and make any needed callback once all the windows, @@ -62,7 +64,8 @@ class WindowManager(object): self._gui_lock = False self._guikit = '' else: - raise RuntimeError('Only the toolkit that owns the lock may release it') + raise RuntimeError( + 'Only the toolkit that owns the lock may release it') def add_window(self, win): self._check_locked() @@ -138,13 +141,13 @@ def prepare_for_display(npy_img): if npy_img.ndim == 2 or \ (npy_img.ndim == 3 and npy_img.shape[2] == 1): npy_plane = npy_img.reshape((height, width)) - out[:,:,0] = npy_plane - out[:,:,1] = npy_plane - out[:,:,2] = npy_plane + out[:, :, 0] = npy_plane + out[:, :, 1] = npy_plane + out[:, :, 2] = npy_plane elif npy_img.ndim == 3: if npy_img.shape[2] == 3 or npy_img.shape[2] == 4: - out[:,:,:3] = npy_img[:,:,:3] + out[:, :, :3] = npy_img[:, :, :3] else: raise ValueError('Image must have 1, 3, or 4 channels') @@ -184,6 +187,7 @@ class ImgThread(threading.Thread): def run(self): self.func(*self.args) + class ThreadDispatch(object): def __init__(self, img, stateimg, func, *args): @@ -197,21 +201,21 @@ class ThreadDispatch(object): self.chunks.append((img, stateimg)) elif self.cores >= 4: - self.chunks.append((img[:(height/4), :, :], - stateimg[:(height/4), :, :])) - self.chunks.append((img[(height/4):(height/2), :, :], - stateimg[(height/4):(height/2), :, :])) - self.chunks.append((img[(height/2):(3*height/4), :, :], - stateimg[(height/2):(3*height/4), :, :])) - self.chunks.append((img[(3*height/4):, :, :], - stateimg[(3*height/4):, :, :])) + self.chunks.append((img[:(height / 4), :, :], + stateimg[:(height / 4), :, :])) + self.chunks.append((img[(height / 4):(height / 2), :, :], + stateimg[(height / 4):(height / 2), :, :])) + self.chunks.append((img[(height / 2):(3 * height / 4), :, :], + stateimg[(height / 2):(3 * height / 4), :, :])) + self.chunks.append((img[(3 * height / 4):, :, :], + stateimg[(3 * height / 4):, :, :])) # if they dont have 1, or 4 or more, 2 is good. else: - self.chunks.append((img[:(height/2), :, :], - stateimg[:(height/2), :, :])) - self.chunks.append((img[(height/2):, :, :], - stateimg[(height/2):, :, :])) + self.chunks.append((img[:(height / 2), :, :], + stateimg[:(height / 2), :, :])) + self.chunks.append((img[(height / 2):, :, :], + stateimg[(height / 2):, :, :])) for i in range(len(self.chunks)): self.threads.append(ImgThread(func, self.chunks[i][0], @@ -224,7 +228,6 @@ class ThreadDispatch(object): t.join() - class ColorMixer(object): ''' a class to manage mixing colors in an image. The input array must be an RGB uint8 image. @@ -300,8 +303,6 @@ class ColorMixer(object): _colormixer.add, channel, ammount) pool.run() - - def multiply(self, channel, ammount): '''Mutliply the indicated channel by the specified value. @@ -320,7 +321,6 @@ class ColorMixer(object): _colormixer.multiply, channel, ammount) pool.run() - def brightness(self, factor, offset): '''Adjust the brightness off an image with an offset and factor. @@ -338,13 +338,11 @@ class ColorMixer(object): _colormixer.brightness, factor, offset) pool.run() - def sigmoid_gamma(self, alpha, beta): pool = ThreadDispatch(self.img, self.stateimg, _colormixer.sigmoid_gamma, alpha, beta) pool.run() - def gamma(self, gamma): pool = ThreadDispatch(self.img, self.stateimg, _colormixer.gamma, gamma) @@ -435,4 +433,3 @@ class ColorMixer(object): ''' R, G, B = _colormixer.py_hsv_2_rgb(H, S, V) return (R, G, B) - diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 19d3ab57..973d9b30 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -118,7 +118,8 @@ class MultiImage(object): if -numframes <= n < numframes: n = n % numframes else: - raise IndexError("There are only %s frames in the image"%numframes) + raise IndexError( + "There are only %s frames in the image" % numframes) if self.conserve_memory: if not self._cached == n: @@ -279,7 +280,8 @@ class ImageCollection(object): if -num <= n < num: n = n % num else: - raise IndexError("There are only %s images in the collection"%num) + raise IndexError( + "There are only %s images in the collection" % num) return n def __iter__(self): diff --git a/skimage/io/setup.py b/skimage/io/setup.py index d8db9e66..55526461 100644 --- a/skimage/io/setup.py +++ b/skimage/io/setup.py @@ -6,6 +6,7 @@ import os.path base_path = os.path.abspath(os.path.dirname(__file__)) + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs @@ -30,10 +31,10 @@ def configuration(parent_package='', top_path=None): if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Image I/O Routines', - url = 'https://github.com/scikits-image/scikits-image', - license = 'Modified BSD', + setup(maintainer='scikits-image Developers', + maintainer_email='scikits-image@googlegroups.com', + description='Image I/O Routines', + url='https://github.com/scikits-image/scikits-image', + license='Modified BSD', **(configuration(top_path='').todict()) ) diff --git a/skimage/io/sift.py b/skimage/io/sift.py index e594da4b..d80ba427 100644 --- a/skimage/io/sift.py +++ b/skimage/io/sift.py @@ -10,6 +10,7 @@ __all__ = ['load_sift', 'load_surf'] import numpy as np + def _sift_read(f, mode='SIFT'): """Read SIFT or SURF features from a file. @@ -56,9 +57,11 @@ def _sift_read(f, mode='SIFT'): return data.view(datatype) + def load_sift(f): return _sift_read(f, mode='SIFT') + def load_surf(f): return _sift_read(f, mode='SURF') diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index fa311e3e..0d420ae7 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -41,7 +41,7 @@ class TestImageCollection(): def return_img(n): return self.collection[n] assert_raises(IndexError, return_img, num) - assert_raises(IndexError, return_img, -num-1) + assert_raises(IndexError, return_img, -num - 1) def test_files_property(self): assert isinstance(self.collection.files, list) @@ -52,6 +52,7 @@ class TestImageCollection(): def test_custom_load(self): load_pattern = [(1, 'one'), (2, 'two')] + def load_fn(x): return x @@ -83,7 +84,7 @@ class TestMultiImage(): def return_img(n): return self.img[n] assert_raises(IndexError, return_img, num) - assert_raises(IndexError, return_img, -num-1) + assert_raises(IndexError, return_img, -num - 1) @skipif(not PIL_available) def test_files_property(self): @@ -102,10 +103,5 @@ class TestMultiImage(): assert_raises(AttributeError, set_mem, True) - - - - if __name__ == "__main__": run_module_suite() - diff --git a/skimage/io/tests/test_colormixer.py b/skimage/io/tests/test_colormixer.py index bf0363c5..f83a34d5 100644 --- a/skimage/io/tests/test_colormixer.py +++ b/skimage/io/tests/test_colormixer.py @@ -134,7 +134,5 @@ class TestColorMixer(object): assert_equal(self.img, np.zeros_like(self.state)) - - if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/tests/test_fits.py b/skimage/io/tests/test_fits.py index bf918882..d432b611 100644 --- a/skimage/io/tests/test_fits.py +++ b/skimage/io/tests/test_fits.py @@ -15,6 +15,7 @@ except ImportError: else: import skimage.io._plugins.fits_plugin as fplug + def test_fits_plugin_import(): # Make sure we get an import exception if PyFITS isn't there # (not sure how useful this is, but it ensures there isn't some other @@ -36,14 +37,16 @@ def test_imread_MEF(): io.use_plugin('fits') testfile = os.path.join(data_dir, 'multi.fits') img = io.imread(testfile) - assert np.all(img==pyfits.getdata(testfile, 1)) + assert np.all(img == pyfits.getdata(testfile, 1)) + @skipif(not pyfits_available) def test_imread_simple(): io.use_plugin('fits') testfile = os.path.join(data_dir, 'simple.fits') img = io.imread(testfile) - assert np.all(img==pyfits.getdata(testfile, 0)) + assert np.all(img == pyfits.getdata(testfile, 0)) + @skipif(not pyfits_available) def test_imread_collection_single_MEF(): @@ -54,6 +57,7 @@ def test_imread_collection_single_MEF(): load_func=fplug.FITSFactory) assert _same_ImageCollection(ic1, ic2) + @skipif(not pyfits_available) def test_imread_collection_MEF_and_simple(): io.use_plugin('fits') @@ -65,6 +69,7 @@ def test_imread_collection_MEF_and_simple(): load_func=fplug.FITSFactory) assert _same_ImageCollection(ic1, ic2) + def _same_ImageCollection(collection1, collection2): """Ancillary function to compare two ImageCollection objects, checking that their constituent arrays are equal. @@ -79,4 +84,3 @@ def _same_ImageCollection(collection1, collection2): if __name__ == '__main__': run_module_suite() - diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index 3d8d16f4..e58c18cc 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -35,7 +35,8 @@ def teardown(): def test_imread(): img = sio.imread(os.path.join(si.data_dir, 'color.png')) assert img.shape == (370, 371, 3) - assert all(img[274,135] == [0, 130, 253]) + assert all(img[274, 135] == [0, 130, 253]) + @skipif(not FI_available) def test_imread_uint16(): @@ -44,6 +45,7 @@ def test_imread_uint16(): assert img.dtype == np.uint16 assert_array_almost_equal(img, expected) + @skipif(not FI_available) def test_imread_uint16_big_endian(): expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy')) @@ -54,7 +56,7 @@ def test_imread_uint16_big_endian(): class TestSave: def roundtrip(self, dtype, x, suffix): - f = NamedTemporaryFile(suffix='.'+suffix) + f = NamedTemporaryFile(suffix='.' + suffix) fname = f.name f.close() sio.imsave(fname, x) @@ -64,12 +66,12 @@ class TestSave: @skipif(not FI_available) def test_imsave_roundtrip(self): for shape, dtype, format in [ - [(10, 10), (np.uint8, np.uint16), ('tif', 'png')], - [(10, 10), (np.float32,), ('tif',)], - [(10, 10, 3), (np.uint8,), ('png',)], + [(10, 10), (np.uint8, np.uint16), ('tif', 'png')], + [(10, 10), (np.float32,), ('tif',)], + [(10, 10, 3), (np.uint8,), ('png',)], [(10, 10, 4), (np.uint8,), ('png',)] ]: - tests = [(d,f) for d in dtype for f in format] + tests = [(d, f) for d in dtype for f in format] for d, f in tests: x = np.ones(shape, dtype=d) * np.random.random(shape) if not np.issubdtype(d, float): @@ -83,7 +85,8 @@ def test_metadata(): assert meta[('EXIF_MAIN', 'Orientation')] == 1 assert meta[('EXIF_MAIN', 'Software')].startswith('ImageMagick') - meta = fi.read_multipage_metadata(os.path.join(si.data_dir, 'multipage.tif')) + meta = fi.read_multipage_metadata( + os.path.join(si.data_dir, 'multipage.tif')) assert len(meta) == 2 assert meta[0][('EXIF_MAIN', 'Orientation')] == 1 assert meta[1][('EXIF_MAIN', 'Software')].startswith('ImageMagick') diff --git a/skimage/io/tests/test_histograms.py b/skimage/io/tests/test_histograms.py index 11602f3a..4ec099e3 100644 --- a/skimage/io/tests/test_histograms.py +++ b/skimage/io/tests/test_histograms.py @@ -4,20 +4,21 @@ import numpy as np import skimage.io._plugins._colormixer as cm from skimage.io._plugins._histograms import histograms + class TestHistogram: def test_basic(self): img = np.ones((50, 50, 3), dtype=np.uint8) r, g, b, v = histograms(img, 255) for band in (r, g, b, v): - yield assert_equal, band.sum(), 50*50 + yield assert_equal, band.sum(), 50 * 50 def test_counts(self): channel = np.arange(255).reshape(51, 5) img = np.empty((51, 5, 3), dtype='uint8') - img[:,:,0] = channel - img[:,:,1] = channel - img[:,:,2] = channel + img[:, :, 0] = channel + img[:, :, 1] = channel + img[:, :, 2] = channel r, g, b, v = histograms(img, 255) assert_array_equal(r, g) assert_array_equal(r, b) diff --git a/skimage/io/tests/test_io.py b/skimage/io/tests/test_io.py index ba05d2d3..72f8496a 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -3,12 +3,14 @@ import numpy as np import skimage.io as io + def test_stack_basic(): x = np.arange(12).reshape(3, 4) io.push(x) assert_array_equal(io.pop(), x) + @raises(ValueError) def test_stack_non_array(): io.push([[1, 2, 3]]) diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 47126fce..e442c856 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -32,6 +32,7 @@ def setup_module(self): except ImportError: pass + @skipif(not PIL_available) def test_imread_flatten(): # a color image is flattened @@ -42,6 +43,7 @@ def test_imread_flatten(): # check that flattening does not occur for an image that is grey already. assert np.sctype2char(img.dtype) in np.typecodes['AllInteger'] + @skipif(not PIL_available) def test_imread_palette(): img = imread(os.path.join(data_dir, 'palette_gray.png')) @@ -49,6 +51,7 @@ def test_imread_palette(): img = imread(os.path.join(data_dir, 'palette_color.png')) assert img.ndim == 3 + @skipif(not PIL_available) def test_palette_is_gray(): from PIL import Image @@ -57,6 +60,7 @@ def test_palette_is_gray(): color = Image.open(os.path.join(data_dir, 'palette_color.png')) assert not _palette_is_grayscale(color) + @skipif(not PIL_available) def test_bilevel(): expected = np.zeros((10, 10)) @@ -65,6 +69,7 @@ def test_bilevel(): img = imread(os.path.join(data_dir, 'checker_bilevel.png')) assert_array_equal(img, expected) + @skipif(not PIL_available) def test_imread_uint16(): expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) @@ -72,6 +77,7 @@ def test_imread_uint16(): assert np.issubdtype(img.dtype, np.uint16) assert_array_almost_equal(img, expected) + # Big endian images not correctly loaded for PIL < 1.1.7 # Renable test when PIL 1.1.7 is more common. @skipif(True) diff --git a/skimage/io/tests/test_plugin.py b/skimage/io/tests/test_plugin.py index 6480c922..28d8c2b3 100644 --- a/skimage/io/tests/test_plugin.py +++ b/skimage/io/tests/test_plugin.py @@ -20,11 +20,13 @@ except OSError: def setup_module(self): - plugin.use('test') # see ../_plugins/test_plugin.py + plugin.use('test') # see ../_plugins/test_plugin.py + def teardown_module(self): io.reset_plugins() + class TestPlugin: def test_read(self): io.imread('test.png', as_grey=True, dtype='i4', plugin='test') diff --git a/skimage/io/tests/test_plugin_util.py b/skimage/io/tests/test_plugin_util.py index 0170fcb1..c302b3f2 100644 --- a/skimage/io/tests/test_plugin_util.py +++ b/skimage/io/tests/test_plugin_util.py @@ -3,6 +3,7 @@ from skimage.io._plugins.util import prepare_for_display, WindowManager from numpy.testing import * import numpy as np + class TestPrepareForDisplay: def test_basic(self): prepare_for_display(np.random.random((10, 10))) @@ -12,7 +13,8 @@ class TestPrepareForDisplay: assert x.dtype == np.dtype(np.uint8) def test_grey(self): - x = prepare_for_display(np.arange(12, dtype=float).reshape((4,3))/11.) + x = prepare_for_display( + np.arange(12, dtype=float).reshape((4, 3)) / 11.) assert_array_equal(x[..., 0], x[..., 2]) assert x[0, 0, 0] == 0 assert x[3, 2, 0] == 255 @@ -31,6 +33,7 @@ class TestPrepareForDisplay: def test_wrong_depth(self): x = prepare_for_display(np.random.random((10, 10, 5))) + class TestWindowManager: callback_called = False diff --git a/skimage/io/tests/test_sift.py b/skimage/io/tests/test_sift.py index b6029461..c488d52f 100644 --- a/skimage/io/tests/test_sift.py +++ b/skimage/io/tests/test_sift.py @@ -7,6 +7,7 @@ import os from skimage.io import load_sift, load_surf + def test_load_sift(): f = NamedTemporaryFile(delete=False) fname = f.name @@ -40,6 +41,7 @@ def test_load_sift(): assert_equal(features['row'][0], 133.92) assert_equal(features['column'][1], 99.75) + def test_load_surf(): f = NamedTemporaryFile(delete=False) fname = f.name diff --git a/skimage/io/video.py b/skimage/io/video.py index 365589c1..92d46e9c 100644 --- a/skimage/io/video.py +++ b/skimage/io/video.py @@ -1,11 +1,13 @@ import numpy as np -import os, time +import os +import time from skimage.io import ImageCollection try: import pygst pygst.require("0.10") - import gst, gobject + import gst + import gobject gobject.threads_init() from gst.extend.discoverer import Discoverer gstreamer_available = True @@ -36,11 +38,11 @@ class CvVideo(object): self.source = source self.capture = cv.CreateFileCapture(self.source) self.size = size - + def get(self): """ Retrieve a video frame as a numpy array. - + Returns ------- output : array (image) @@ -55,31 +57,34 @@ class CvVideo(object): else: cv.Resize(img, cv.fromarray(img_mat)) # opencv stores images in BGR format - cv.CvtColor(cv.fromarray(img_mat), cv.fromarray(img_mat), cv.CV_BGR2RGB) + cv.CvtColor( + cv.fromarray(img_mat), cv.fromarray(img_mat), cv.CV_BGR2RGB) return img_mat - + def seek_frame(self, frame_number): """ Seek to specified frame in video. - + Parameters ---------- frame_number : int Frame position """ - cv.SetCaptureProperty(self.capture, cv.CV_CAP_PROP_POS_FRAMES, frame_number) - + cv.SetCaptureProperty( + self.capture, cv.CV_CAP_PROP_POS_FRAMES, frame_number) + def seek_time(self, milliseconds): """ Seek to specified time in video. - + Parameters ---------- milliseconds : int Time position """ - cv.SetCaptureProperty(self.capture, cv.CV_CAP_PROP_POS_MSEC, milliseconds) - + cv.SetCaptureProperty( + self.capture, cv.CV_CAP_PROP_POS_MSEC, milliseconds) + def frame_count(self): """ Returns frame count of video. @@ -90,7 +95,7 @@ class CvVideo(object): Frame count. """ return cv.GetCaptureProperty(self.capture, cv.CV_CAP_PROP_FRAME_COUNT) - + def duration(self): """ Returns time length of video in milliseconds. @@ -102,8 +107,8 @@ class CvVideo(object): """ return cv.GetCaptureProperty(self.capture, cv.CV_CAP_PROP_FPS) * \ cv.GetCaptureProperty(self.capture, cv.CV_CAP_PROP_FRAME_COUNT) - - + + class GstVideo(object): """ GStreamer-based video loader. @@ -127,7 +132,7 @@ class GstVideo(object): self.video_length = 0 self.video_rate = 0 # extract video size - if not size: + if not size: gobject.idle_add(self._discover_one) self.mainloop = gobject.MainLoop() self.mainloop.run() @@ -143,7 +148,7 @@ class GstVideo(object): """ discoverer = Discoverer(self.source) discoverer.connect('discovered', self._discovered) - discoverer.discover() + discoverer.discover() return False def _discovered(self, d, is_media): @@ -152,13 +157,13 @@ class GstVideo(object): """ if is_media: self.size = (d.videowidth, d.videoheight) - self.video_length = d.videolength / gst.MSECOND + self.video_length = d.videolength / gst.MSECOND self.video_rate = d.videorate.num self.mainloop.quit() return False - + def _create_main_pipeline(self, source, size, sync): - """ + """ Create the frame extraction pipeline. """ pipeline_string = "uridecodebin name=decoder uri=%s ! ffmpegcolorspace ! videoscale ! appsink name=play_sink" % self.source @@ -174,11 +179,11 @@ class GstVideo(object): if self.pipeline.set_state(gst.STATE_PLAYING) == gst.STATE_CHANGE_FAILURE: raise NameError("Failed to load video source %s" % self.source) buff = self.appsink.emit('pull-preroll') - + def get(self): """ Retrieve a video frame as a numpy array. - + Returns ------- output : array (image) @@ -191,24 +196,24 @@ class GstVideo(object): def seek_frame(self, frame_number): """ Seek to specified frame in video. - + Parameters ---------- frame_number : int Frame position """ self.pipeline.seek_simple(gst.FORMAT_DEFAULT, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, frame_number) - + def seek_time(self, milliseconds): """ Seek to specified time in video. - + Parameters ---------- milliseconds : int Time position """ - self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, milliseconds/1000.0 * gst.SECOND) + self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, milliseconds / 1000.0 * gst.SECOND) def frame_count(self): """ @@ -219,8 +224,8 @@ class GstVideo(object): output : int Frame count. """ - return self.video_length/1000*self.video_rate - + return self.video_length / 1000 * self.video_rate + def duration(self): """ Returns time length of video in milliseconds. @@ -236,7 +241,7 @@ class GstVideo(object): class Video(object): """ Video loader. Supports Opencv and Gstreamer backends. - + Parameters ---------- source : str @@ -248,7 +253,7 @@ class Video(object): If enabled the video time step continues onward according to the play rate. Useful for IP cameras and other real time video feeds. backend: str, 'gstreamer' or 'opencv' - Backend to use. + Backend to use. """ def __init__(self, source=None, size=None, sync=False, backend=None): if backend == None: @@ -270,29 +275,29 @@ class Video(object): def get(self): """ Retrieve the next video frame as a numpy array. - + Returns ------- output : array (image) Retrieved image. """ return self.video.get() - + def seek_frame(self, frame_number): """ Seek to specified frame in video. - + Parameters ---------- frame_number : int Frame position """ self.video.seek_frame(frame_number) - + def seek_time(self, milliseconds): """ Seek to specified time in video. - + Parameters ---------- milliseconds : int @@ -310,7 +315,7 @@ class Video(object): Frame count. """ return self.video.frame_count() - + def duration(self): """ Returns time length of video in milliseconds. @@ -321,11 +326,11 @@ class Video(object): Time length [ms]. """ return self.video.duration() - + def get_index_frame(self, frame_number): """ Retrieve a specified video frame as a numpy array. - + Parameters ---------- frame_number : int @@ -335,28 +340,27 @@ class Video(object): ------- output : array (image) Retrieved image. - """ + """ self.video.seek_frame(frame_number) return self.video.get() - + def get_collection(self, time_range=None): """ Returns an ImageCollection object. - + Parameters ---------- time_range: range (int), optional Time steps to extract, defaults to the entire length of video. - + Returns ------- - output: ImageCollection + output: ImageCollection Collection of images iterator. """ if not time_range: time_range = range(int(self.frame_count())) return ImageCollection(time_range, load_func=self.get_index_frame) - - -__all__ = ["Video"] + +__all__ = ["Video"] diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 937b80f5..89dbddce 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -201,14 +201,14 @@ def regionprops(label_image, properties=['Area', 'Centroid'], m = _moments.central_moments(array, 0, 0, 3) # centroid - cr = m[0,1] / m[0,0] - cc = m[1,0] / m[0,0] + cr = m[0, 1] / m[0, 0] + cc = m[1, 0] / m[0, 0] mu = _moments.central_moments(array, cr, cc, 3) #: elements of the inertia tensor [a b; b c] - a = mu[2,0] / mu[0,0] - b = mu[1,1] / mu[0,0] - c = mu[0,2] / mu[0,0] + a = mu[2, 0] / mu[0, 0] + b = mu[1, 1] / mu[0, 0] + c = mu[0, 2] / mu[0, 0] #: eigen values of inertia tensor l1 = (a + c) / 2 + sqrt(4 * b ** 2 + (a - c) ** 2) / 2 l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 @@ -219,7 +219,7 @@ def regionprops(label_image, properties=['Area', 'Centroid'], _nu = None if 'Area' in properties: - obj_props['Area'] = m[0,0] + obj_props['Area'] = m[0, 0] if 'BoundingBox' in properties: obj_props['BoundingBox'] = (r0, c0, sl[0].stop, sl[1].stop) @@ -247,17 +247,17 @@ def regionprops(label_image, properties=['Area', 'Centroid'], obj_props['Eccentricity'] = sqrt(1 - l2 / l1) if 'EquivDiameter' in properties: - obj_props['EquivDiameter'] = sqrt(4 * m[0,0] / PI) + obj_props['EquivDiameter'] = sqrt(4 * m[0, 0] / PI) if 'EulerNumber' in properties: if _filled_image is None: _filled_image = ndimage.binary_fill_holes(array, STREL_8) euler_array = _filled_image != array _, num = ndimage.label(euler_array, STREL_8) - obj_props['EulerNumber'] = - num + obj_props['EulerNumber'] = - num if 'Extent' in properties: - obj_props['Extent'] = m[0,0] / (array.shape[0] * array.shape[1]) + obj_props['Extent'] = m[0, 0] / (array.shape[0] * array.shape[1]) if 'HuMoments' in properties: if _nu is None: @@ -300,16 +300,15 @@ def regionprops(label_image, properties=['Area', 'Centroid'], if 'Solidity' in properties: if _convex_image is None: _convex_image = convex_hull_image(array) - obj_props['Solidity'] = m[0,0] / np.sum(_convex_image) - + obj_props['Solidity'] = m[0, 0] / np.sum(_convex_image) if intensity_image is not None: weighted_array = array * intensity_image[sl] wm = _moments.central_moments(weighted_array, 0, 0, 3) # weighted centroid - wcr = wm[0,1] / wm[0,0] - wcc = wm[1,0] / wm[0,0] + wcr = wm[0, 1] / wm[0, 0] + wcc = wm[1, 0] / wm[0, 0] wmu = _moments.central_moments(weighted_array, wcr, wcc, 3) # cached results which are used by several properties diff --git a/skimage/measure/find_contours.py b/skimage/measure/find_contours.py index 92f848d2..a64d63ea 100755 --- a/skimage/measure/find_contours.py +++ b/skimage/measure/find_contours.py @@ -101,8 +101,8 @@ def find_contours(array, level, level = float(level) if (fully_connected not in _param_options or positive_orientation not in _param_options): - raise ValueError('Parameters "fully_connected" and' - ' "positive_orientation" must be either "high" or "low".') + raise ValueError('Parameters "fully_connected" and' + ' "positive_orientation" must be either "high" or "low".') point_list = _find_contours.iterate_and_store(array, level, fully_connected == 'high') contours = _assemble_contours(_take_2(point_list)) @@ -110,12 +110,14 @@ def find_contours(array, level, contours = [c[::-1] for c in contours] return contours + def _take_2(seq): - iterator = iter(seq) - while(True): - n1 = iterator.next() - n2 = iterator.next() - yield (n1, n2) + iterator = iter(seq) + while(True): + n1 = iterator.next() + n2 = iterator.next() + yield (n1, n2) + def _assemble_contours(points_iterator): current_index = 0 @@ -143,7 +145,7 @@ def _assemble_contours(points_iterator): head.append(to_point) del starts[to_point] del ends[from_point] - else: # tail is not head + else: # tail is not head # We need to join two distinct contours. # We want to keep the first contour segment created, so that # the final contours are ordered left->right, top->bottom. @@ -157,7 +159,7 @@ def _assemble_contours(points_iterator): # remove the old end of head and add the new end. del ends[from_point] ends[head[-1]] = (head, head_num) - else: # tail_num <= head_num + else: # tail_num <= head_num # head was created second. Prepend head to tail. tail.extendleft(reversed(head)) # remove all traces of head: diff --git a/skimage/measure/setup.py b/skimage/measure/setup.py index 4b02a1d1..7e4ecf0f 100644 --- a/skimage/measure/setup.py +++ b/skimage/measure/setup.py @@ -5,6 +5,7 @@ from skimage._build import cython import os 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 @@ -23,10 +24,10 @@ def configuration(parent_package='', top_path=None): if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Graph-based Image-processing Algorithms', - url = 'https://github.com/scikits-image/scikits-image', - license = 'Modified BSD', + setup(maintainer='scikits-image Developers', + maintainer_email='scikits-image@googlegroups.com', + description='Graph-based Image-processing Algorithms', + url='https://github.com/scikits-image/scikits-image', + license='Modified BSD', **(configuration(top_path='').todict()) ) diff --git a/skimage/measure/tests/test_find_contours.py b/skimage/measure/tests/test_find_contours.py index 8d705878..1c6096ee 100644 --- a/skimage/measure/tests/test_find_contours.py +++ b/skimage/measure/tests/test_find_contours.py @@ -1,9 +1,9 @@ import numpy as np from numpy.testing import * -from skimage.measure import find_contours +from skimage.measure import find_contours -a = np.ones((8,8), dtype=np.float32) +a = np.ones((8, 8), dtype=np.float32) a[1:-1, 1] = 0 a[1, 1:-1] = 0 @@ -16,8 +16,9 @@ a[1, 1:-1] = 0 ## [ 1., 0., 1., 1., 1., 1., 1., 1.], ## [ 1., 1., 1., 1., 1., 1., 1., 1.]], dtype=float32) -x,y = np.mgrid[-1:1:5j,-1:1:5j] -r = np.sqrt(x**2 + y**2) +x, y = np.mgrid[-1:1:5j, -1:1:5j] +r = np.sqrt(x ** 2 + y ** 2) + def test_binary(): contours = find_contours(a, 0.5) diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index 4d040b5c..b9b16dff 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -18,19 +18,20 @@ SAMPLE = np.array( [0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]] ) INTENSITY_SAMPLE = SAMPLE.copy() -INTENSITY_SAMPLE[1,9:11] = 2 +INTENSITY_SAMPLE[1, 9:11] = 2 def test_area(): area = regionprops(SAMPLE, ['Area'])[0]['Area'] assert area == np.sum(SAMPLE) + def test_bbox(): bbox = regionprops(SAMPLE, ['BoundingBox'])[0]['BoundingBox'] assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1])) SAMPLE_mod = SAMPLE.copy() - SAMPLE_mod[:,-1] = 0 + SAMPLE_mod[:, -1] = 0 bbox = regionprops(SAMPLE_mod, ['BoundingBox'])[0]['BoundingBox'] assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1]-1)) @@ -51,11 +52,13 @@ def test_centroid(): # determined with MATLAB assert_array_almost_equal(centroid, (5.66666666666666, 9.444444444444444)) + def test_convex_area(): area = regionprops(SAMPLE, ['ConvexArea'])[0]['ConvexArea'] # determined with MATLAB assert area == 124 + def test_convex_image(): img = regionprops(SAMPLE, ['ConvexImage'])[0]['ConvexImage'] # determined with MATLAB @@ -73,28 +76,33 @@ def test_convex_image(): ) assert_array_equal(img, ref) + def test_eccentricity(): eps = regionprops(SAMPLE, ['Eccentricity'])[0]['Eccentricity'] assert_almost_equal(eps, 0.814629313427) + def test_equiv_diameter(): diameter = regionprops(SAMPLE, ['EquivDiameter'])[0]['EquivDiameter'] # determined with MATLAB assert_almost_equal(diameter, 9.57461472963) + def test_euler_number(): en = regionprops(SAMPLE, ['EulerNumber'])[0]['EulerNumber'] assert en == 0 SAMPLE_mod = SAMPLE.copy() - SAMPLE_mod[7,-3] = 0 + SAMPLE_mod[7, -3] = 0 en = regionprops(SAMPLE_mod, ['EulerNumber'])[0]['EulerNumber'] assert en == -1 + def test_extent(): extent = regionprops(SAMPLE, ['Extent'])[0]['Extent'] assert_almost_equal(extent, 0.4) + def test_hu_moments(): hu = regionprops(SAMPLE, ['HuMoments'])[0]['HuMoments'] ref = np.array([ @@ -109,46 +117,54 @@ def test_hu_moments(): # bug in OpenCV caused in Central Moments calculation? assert_array_almost_equal(hu, ref) + def test_image(): img = regionprops(SAMPLE, ['Image'])[0]['Image'] assert_array_equal(img, SAMPLE) + def test_filled_area(): area = regionprops(SAMPLE, ['FilledArea'])[0]['FilledArea'] assert area == np.sum(SAMPLE) SAMPLE_mod = SAMPLE.copy() - SAMPLE_mod[7,-3] = 0 + SAMPLE_mod[7, -3] = 0 area = regionprops(SAMPLE_mod, ['FilledArea'])[0]['FilledArea'] assert area == np.sum(SAMPLE) + def test_major_axis_length(): length = regionprops(SAMPLE, ['MajorAxisLength'])[0]['MajorAxisLength'] # MATLAB has different interpretation of ellipse than found in literature, # here implemented as found in literature assert_almost_equal(length, 16.7924234999) + def test_max_intensity(): intensity = regionprops(SAMPLE, ['MaxIntensity'], INTENSITY_SAMPLE )[0]['MaxIntensity'] assert_almost_equal(intensity, 2) + def test_mean_intensity(): intensity = regionprops(SAMPLE, ['MeanIntensity'], INTENSITY_SAMPLE )[0]['MeanIntensity'] assert_almost_equal(intensity, 1.02777777777777) + def test_min_intensity(): intensity = regionprops(SAMPLE, ['MinIntensity'], INTENSITY_SAMPLE )[0]['MinIntensity'] assert_almost_equal(intensity, 1) + def test_minor_axis_length(): length = regionprops(SAMPLE, ['MinorAxisLength'])[0]['MinorAxisLength'] # MATLAB has different interpretation of ellipse than found in literature, # here implemented as found in literature assert_almost_equal(length, 9.739302807263) + def test_moments(): m = regionprops(SAMPLE, ['Moments'])[0]['Moments'] #: determined with OpenCV @@ -199,11 +215,13 @@ def test_weighted_central_moments(): np.set_printoptions(precision=10) assert_array_almost_equal(wmu, ref) + def test_weighted_centroid(): centroid = regionprops(SAMPLE, ['WeightedCentroid'], INTENSITY_SAMPLE )[0]['WeightedCentroid'] assert_array_almost_equal(centroid, (5.540540540540, 9.445945945945)) + def test_weighted_hu_moments(): whu = regionprops(SAMPLE, ['WeightedHuMoments'], INTENSITY_SAMPLE )[0]['WeightedHuMoments'] @@ -218,6 +236,7 @@ def test_weighted_hu_moments(): ]) assert_array_almost_equal(whu, ref) + def test_weighted_moments(): wm = regionprops(SAMPLE, ['WeightedMoments'], INTENSITY_SAMPLE )[0]['WeightedMoments'] diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index f461dc4d..6c4797ef 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -4,6 +4,7 @@ import numpy as np from ._pnpoly import points_inside_poly, grid_points_inside_poly from ._convex_hull import possible_hull + def convex_hull_image(image): """Compute the convex hull image of a binary image. @@ -34,7 +35,7 @@ def convex_hull_image(image): # hull. coords = possible_hull(image.astype(np.uint8)) N = len(coords) - + # Add a vertex for the middle of each pixel edge coords_corners = np.empty((N * 4, 2)) for i, (x_offset, y_offset) in enumerate(zip((0, 0, -0.5, 0.5), @@ -61,5 +62,5 @@ def convex_hull_image(image): # For each pixel coordinate, check whether that pixel # lies inside the convex hull mask = grid_points_inside_poly(image.shape[:2], v) - + return mask diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index ee8542dd..309fb5be 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -71,7 +71,7 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): import skimage.morphology.cmorph as cmorph out = cmorph.erode(image, selem, out=out, shift_x=shift_x, shift_y=shift_y) - return out; + return out except ImportError: raise ImportError("cmorph extension not available.") @@ -130,7 +130,7 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): from . import cmorph out = cmorph.dilate(image, selem, out=out, shift_x=shift_x, shift_y=shift_y) - return out; + return out except ImportError: raise ImportError("cmorph extension not available.") @@ -342,23 +342,27 @@ def greyscale_erode(*args, **kwargs): warnings.warn("`greyscale_erode` renamed `erosion`.") return erosion(*args, **kwargs) + def greyscale_dilate(*args, **kwargs): warnings.warn("`greyscale_dilate` renamed `dilation`.") return dilation(*args, **kwargs) + def greyscale_open(*args, **kwargs): warnings.warn("`greyscale_open` renamed `opening`.") return opening(*args, **kwargs) + def greyscale_close(*args, **kwargs): warnings.warn("`greyscale_close` renamed `closing`.") return closing(*args, **kwargs) + def greyscale_white_top_hat(*args, **kwargs): warnings.warn("`greyscale_white_top_hat` renamed `white_tophat`.") return white_tophat(*args, **kwargs) + def greyscale_black_top_hat(*args, **kwargs): warnings.warn("`greyscale_black_top_hat` renamed `black_tophat`.") return black_tophat(*args, **kwargs) - diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index ce58fbfa..5ff6793a 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -5,6 +5,7 @@ import numpy as np + def square(width, dtype=np.uint8): """ Generates a flat, square-shaped structuring element. Every pixel @@ -30,6 +31,7 @@ def square(width, dtype=np.uint8): """ return np.ones((width, width), dtype=dtype) + def rectangle(width, height, dtype=np.uint8): """ Generates a flat, rectangular-shaped structuring element of a @@ -58,6 +60,7 @@ def rectangle(width, height, dtype=np.uint8): """ return np.ones((width, height), dtype=dtype) + def diamond(radius, dtype=np.uint8): """ Generates a flat, diamond-shaped structuring element of a given @@ -75,22 +78,23 @@ def diamond(radius, dtype=np.uint8): Returns ------- - + selem : ndarray The structuring element where elements of the neighborhood are 1 and 0 otherwise. """ half = radius - (I, J) = np.meshgrid(xrange(0, radius*2+1), xrange(0, radius*2+1)) - s = np.abs(I-half)+np.abs(J-half) + (I, J) = np.meshgrid(xrange(0, radius * 2 + 1), xrange(0, radius * 2 + 1)) + s = np.abs(I - half) + np.abs(J - half) return np.array(s <= radius, dtype=dtype) - + + def disk(radius, dtype=np.uint8): """ Generates a flat, disk-shaped structuring element of a given radius. A pixel is within the neighborhood if the euclidean distance between it and the origin is no greater than a radius. - + Parameters ---------- radius : int @@ -103,10 +107,10 @@ def disk(radius, dtype=np.uint8): ------- selem : ndarray The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + are 1 and 0 otherwise. """ - L = np.linspace(-radius, radius, 2*radius+1) + L = np.linspace(-radius, radius, 2 * radius + 1) (X, Y) = np.meshgrid(L, L) - s = X**2 - s += Y**2 + s = X ** 2 + s += Y ** 2 return np.array(s <= radius * radius, dtype=dtype) diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index 6f227a71..fcf33f7f 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -5,6 +5,7 @@ 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 @@ -35,11 +36,11 @@ def configuration(parent_package='', top_path=None): if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - author = 'Damian Eads', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Morphology Wrapper', - url = 'https://github.com/scikits-image/scikits-image', - license = 'SciPy License (BSD Style)', + setup(maintainer='scikits-image Developers', + author='Damian Eads', + maintainer_email='scikits-image@googlegroups.com', + description='Morphology Wrapper', + url='https://github.com/scikits-image/scikits-image', + license='SciPy License (BSD Style)', **(configuration(top_path='').todict()) ) diff --git a/skimage/morphology/skeletonize.py b/skimage/morphology/skeletonize.py index e734d8c6..b7c59ac1 100644 --- a/skimage/morphology/skeletonize.py +++ b/skimage/morphology/skeletonize.py @@ -9,31 +9,32 @@ from ._skeletonize import _skeletonize_loop, _table_lookup_index # --------- Skeletonization by morphological thinning --------- + def skeletonize(image): """Return the skeleton of a binary image. - + Thinning is used to reduce each connected component in a binary image - to a single-pixel wide skeleton. - + to a single-pixel wide skeleton. + Parameters ---------- - image : numpy.ndarray - A binary image containing the objects to be skeletonized. '1' - represents foreground, and '0' represents background. It + image : numpy.ndarray + A binary image containing the objects to be skeletonized. '1' + represents foreground, and '0' represents background. It also accepts arrays of boolean values where True is foreground. - + Returns ------- skeleton : ndarray A matrix containing the thinned image. - + See also -------- medial_axis Notes ----- - The algorithm [1] works by making successive passes of the image, + The algorithm [1] works by making successive passes of the image, removing pixels on object borders. This continues until no more pixels can be removed. The image is correlated with a mask that assigns each pixel a number in the range [0...255] @@ -41,18 +42,18 @@ def skeletonize(image): pixels. A look up table is then used to assign the pixels a value of 0, 1, 2 or 3, which are selectively removed during the iterations. - - Note that this algorithm will give different results than a + + Note that this algorithm will give different results than a medial axis transform, which is also often referred to as - "skeletonization". - + "skeletonization". + References ---------- - .. [1] A fast parallel algorithm for thinning digital patterns, + .. [1] A fast parallel algorithm for thinning digital patterns, T. Y. ZHANG and C. Y. SUEN, Communications of the ACM, March 1984, Volume 27, Number 3 - + Examples -------- >>> X, Y = np.ogrid[0:9, 0:9] @@ -97,38 +98,38 @@ def skeletonize(image): # check some properties of the input image: # - 2D - # - binary image with only 0's and 1's + # - binary image with only 0's and 1's if skeleton.ndim != 2: - raise ValueError('Skeletonize requires a 2D array') + raise ValueError('Skeletonize requires a 2D array') if not np.all(np.in1d(skeleton.flat, (0, 1))): raise ValueError('Image contains values other than 0 and 1') - + # create the mask that will assign a unique value based on the # arrangement of neighbouring pixels - mask = np.array([[ 1, 2, 4], + mask = np.array([[1, 2, 4], [128, 0, 8], - [ 64, 32, 16]], np.uint8) + [64, 32, 16]], np.uint8) pixelRemoved = True while pixelRemoved: - pixelRemoved = False; + pixelRemoved = False # assign each pixel a unique value based on its foreground neighbours neighbours = ndimage.correlate(skeleton, mask, mode='constant') - + # ignore background neighbours *= skeleton - + # use LUT to categorize each foreground pixel as a 0, 1, 2 or 3 codes = np.take(lut, neighbours) - + # pass 1 - remove the 1's and 3's code_mask = (codes == 1) - if np.any(code_mask): + if np.any(code_mask): pixelRemoved = True skeleton[code_mask] = 0 code_mask = (codes == 3) - if np.any(code_mask): + if np.any(code_mask): pixelRemoved = True skeleton[code_mask] = 0 @@ -137,11 +138,11 @@ def skeletonize(image): neighbours *= skeleton codes = np.take(lut, neighbours) code_mask = (codes == 2) - if np.any(code_mask): + if np.any(code_mask): pixelRemoved = True skeleton[code_mask] = 0 code_mask = (codes == 3) - if np.any(code_mask): + if np.any(code_mask): pixelRemoved = True skeleton[code_mask] = 0 @@ -159,8 +160,8 @@ def medial_axis(image, mask=None, return_distance=False): Parameters ---------- - image : binary ndarray - + image : binary ndarray + mask : binary ndarray, optional If a mask is given, only those elements with a true value in `mask` are used for computing the medial axis. @@ -177,18 +178,18 @@ def medial_axis(image, mask=None, return_distance=False): dist : ndarray of ints Distance transform of the image (only returned if `return_distance` is True) - + See also -------- - skeletonize + skeletonize Notes ----- This algorithm computes the medial axis transform of an image - as the ridges of its distance transform. - + as the ridges of its distance transform. + The different steps of the algorithm are as follows - * A lookup table is used, that assigns 0 or 1 to each configuration of + * A lookup table is used, that assigns 0 or 1 to each configuration of the 3x3 binary square, whether the central pixel should be removed or kept. We want a point to be removed if it has more than one neighbor and if removing it does not change the number of connected components. @@ -197,12 +198,12 @@ def medial_axis(image, mask=None, return_distance=False): the cornerness of the pixel. * The foreground (value of 1) points are ordered by - the distance transform, then the cornerness. - - * A cython function is called to reduce the image to its skeleton. It - processes pixels in the order determined at the previous step, and - removes or maintains a pixel according to the lookup table. Because - of the ordering, it is possible to process all pixels in only one + the distance transform, then the cornerness. + + * A cython function is called to reduce the image to its skeleton. It + processes pixels in the order determined at the previous step, and + removes or maintains a pixel according to the lookup table. Because + of the ordering, it is possible to process all pixels in only one pass. Examples @@ -234,7 +235,7 @@ def medial_axis(image, mask=None, return_distance=False): masked_image = image.astype(bool).copy() masked_image[~mask] = False # - # Build lookup table - three conditions + # Build lookup table - three conditions # 1. Keep only positive pixels (center_is_foreground array). # AND # 2. Keep if removing the pixel results in a different connectivity @@ -243,36 +244,35 @@ def medial_axis(image, mask=None, return_distance=False): # OR # 3. Keep if # pixels in neighbourhood is 2 or less # Note that table is independent of image - center_is_foreground = (np.arange(512) & 2**4).astype(bool) + center_is_foreground = (np.arange(512) & 2 ** 4).astype(bool) table = (center_is_foreground # condition 1. & (np.array([ndimage.label(_pattern_of(index), _eight_connect)[1] != - ndimage.label(_pattern_of(index & ~ 2**4), + ndimage.label(_pattern_of(index & ~ 2 ** 4), _eight_connect)[1] - for index in range(512)]) # condition 2 - | + for index in range(512)]) # condition 2 + | np.array([np.sum(_pattern_of(index)) < 3 for index in range(512)])) # condition 3 ) - - + # Build distance transform distance = ndimage.distance_transform_edt(masked_image) if return_distance: store_distance = distance.copy() - + # Corners # The processing order along the edge is critical to the shape of the # resulting skeleton: if you process a corner first, that corner will # be eroded and the skeleton will miss the arm from that corner. Pixels # with fewer neighbors are more "cornery" and should be processed last. - # We use a cornerness_table lookup table where the score of a + # We use a cornerness_table lookup table where the score of a # configuration is the number of background (0-value) pixels in the # 3x3 neighbourhood cornerness_table = np.array([9 - np.sum(_pattern_of(index)) for index in range(512)]) corner_score = _table_lookup(masked_image, cornerness_table) - + # Define arrays for inner loop i, j = np.mgrid[0:image.shape[0], 0:image.shape[1]] result = masked_image.copy() @@ -280,7 +280,7 @@ def medial_axis(image, mask=None, return_distance=False): i = np.ascontiguousarray(i[result], np.int32) j = np.ascontiguousarray(j[result], np.int32) result = np.ascontiguousarray(result, np.uint8) - + # Determine the order in which pixels are processed. # We use a random # for tiebreaking. Assign each pixel in the image a # predictable, random # so that masking doesn't affect arbitrary choices @@ -305,14 +305,15 @@ def medial_axis(image, mask=None, return_distance=False): else: return result + def _pattern_of(index): """ Return the pattern represented by an index value Byte decomposition of index """ - return np.array([[index & 2**0, index & 2**1, index & 2**2], - [index & 2**3, index & 2**4, index & 2**5], - [index & 2**6, index & 2**7, index & 2**8]], bool) + return np.array([[index & 2 ** 0, index & 2 ** 1, index & 2 ** 2], + [index & 2 ** 3, index & 2 ** 4, index & 2 ** 5], + [index & 2 ** 6, index & 2 ** 7, index & 2 ** 8]], bool) def _table_lookup(image, table): @@ -338,13 +339,14 @@ def _table_lookup(image, table): Notes ----- The pixels are numbered like this:: + 0 1 2 3 4 5 6 7 8 The index at a pixel is the sum of 2** for pixels - that evaluate to true. + that evaluate to true. """ # # We accumulate into the indexer to get the index into the table diff --git a/skimage/morphology/tests/test_convex_hull.py b/skimage/morphology/tests/test_convex_hull.py index 21f45cd7..9ab514d0 100644 --- a/skimage/morphology/tests/test_convex_hull.py +++ b/skimage/morphology/tests/test_convex_hull.py @@ -10,6 +10,7 @@ try: except ImportError: scipy_spatial = False + @skipif(not scipy_spatial) def test_basic(): image = np.array( @@ -30,6 +31,7 @@ def test_basic(): assert_array_equal(convex_hull_image(image), expected) + @skipif(not scipy_spatial) def test_possible_hull(): image = np.array( diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 1103b20d..d7242e8f 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -11,6 +11,7 @@ from skimage.morphology import selem lena = np.load(os.path.join(data_dir, 'lena_GRAY_U8.npy')) + class TestMorphology(): def morph_worker(self, img, fn, morph_func, strel_func): @@ -155,4 +156,3 @@ class TestDTypes(): if __name__ == '__main__': testing.run_module_suite() - diff --git a/skimage/morphology/tests/test_pnpoly.py b/skimage/morphology/tests/test_pnpoly.py index 33efe15f..da468b08 100644 --- a/skimage/morphology/tests/test_pnpoly.py +++ b/skimage/morphology/tests/test_pnpoly.py @@ -4,6 +4,7 @@ from numpy.testing import assert_array_equal from skimage.morphology._pnpoly import points_inside_poly, \ grid_points_inside_poly + class test_npnpoly(): def test_square(self): v = np.array([[0, 0], @@ -24,13 +25,14 @@ class test_npnpoly(): def test_type(self): assert(points_inside_poly([[0, 0]], [[0, 0]]).dtype == np.bool) + def test_grid_points_inside_poly(): v = np.array([[0, 0], [5, 0], [5, 5]]) expected = np.tril(np.ones((5, 5), dtype=bool)) - + assert_array_equal(grid_points_inside_poly((5, 5), v), expected) diff --git a/skimage/morphology/tests/test_selem.py b/skimage/morphology/tests/test_selem.py index 639cdf4e..eec6cd5b 100644 --- a/skimage/morphology/tests/test_selem.py +++ b/skimage/morphology/tests/test_selem.py @@ -10,6 +10,7 @@ from skimage.io import * from skimage import data_dir from skimage.morphology import * + class TestSElem(): def test_square_selem(self): @@ -32,13 +33,12 @@ class TestSElem(): expected_mask = matlab_masks[arrname] actual_mask = func(k) if (expected_mask.shape == (1,)): - expected_mask = expected_mask[:,np.newaxis] + expected_mask = expected_mask[:, np.newaxis] assert_equal(expected_mask, actual_mask) k = k + 1 - + def test_selem_disk(self): self.strel_worker("disk-matlab-output.npz", selem.disk) def test_selem_diamond(self): self.strel_worker("diamond-matlab-output.npz", selem.diamond) - diff --git a/skimage/morphology/tests/test_skeletonize.py b/skimage/morphology/tests/test_skeletonize.py index 539e0ad8..408e971c 100644 --- a/skimage/morphology/tests/test_skeletonize.py +++ b/skimage/morphology/tests/test_skeletonize.py @@ -7,89 +7,92 @@ from skimage.io import imread from skimage import data_dir import os.path + class TestSkeletonize(): def test_skeletonize_no_foreground(self): - im = np.zeros((5,5)) + im = np.zeros((5, 5)) result = skeletonize(im) - numpy.testing.assert_array_equal(result, np.zeros((5,5))) - + numpy.testing.assert_array_equal(result, np.zeros((5, 5))) + def test_skeletonize_wrong_dim1(self): im = np.zeros((5)) - numpy.testing.assert_raises(ValueError, skeletonize, im) + numpy.testing.assert_raises(ValueError, skeletonize, im) def test_skeletonize_wrong_dim2(self): im = np.zeros((5, 5, 5)) - numpy.testing.assert_raises(ValueError, skeletonize, im) + numpy.testing.assert_raises(ValueError, skeletonize, im) def test_skeletonize_not_binary(self): im = np.zeros((5, 5)) im[0, 0] = 1 im[0, 1] = 2 - numpy.testing.assert_raises(ValueError, skeletonize, im) - + numpy.testing.assert_raises(ValueError, skeletonize, im) + def test_skeletonize_unexpected_value(self): im = np.zeros((5, 5)) im[0, 0] = 2 - numpy.testing.assert_raises(ValueError, skeletonize, im) - + numpy.testing.assert_raises(ValueError, skeletonize, im) + def test_skeletonize_all_foreground(self): - im = np.ones((3,4)) + im = np.ones((3, 4)) result = skeletonize(im) - + def test_skeletonize_single_point(self): im = np.zeros((5, 5), np.uint8) im[3, 3] = 1 result = skeletonize(im) numpy.testing.assert_array_equal(result, im) - + def test_skeletonize_already_thinned(self): im = np.zeros((5, 5), np.uint8) - im[3,1:-1] = 1 + im[3, 1:-1] = 1 im[2, -1] = 1 im[4, 0] = 1 result = skeletonize(im) numpy.testing.assert_array_equal(result, im) - + def test_skeletonize_output(self): im = imread(os.path.join(data_dir, "bw_text.png"), as_grey=True) - + # make black the foreground - im = (im==0) + im = (im == 0) result = skeletonize(im) - + expected = np.load(os.path.join(data_dir, "bw_text_skeleton.npy")) numpy.testing.assert_array_equal(result, expected) - - + def test_skeletonize_num_neighbours(self): # an empty image image = np.zeros((300, 300)) - + # foreground object 1 image[10:-10, 10:100] = 1 image[-100:-10, 10:-10] = 1 image[10:-10, -100:-10] = 1 - + # foreground object 2 rs, cs = draw.bresenham(250, 150, 10, 280) - for i in range(10): image[rs+i, cs] = 1 + for i in range(10): + image[rs + i, cs] = 1 rs, cs = draw.bresenham(10, 150, 250, 280) - for i in range(20): image[rs+i, cs] = 1 - + for i in range(20): + image[rs + i, cs] = 1 + # foreground object 3 ir, ic = np.indices(image.shape) - circle1 = (ic - 135)**2 + (ir - 150)**2 < 30**2 - circle2 = (ic - 135)**2 + (ir - 150)**2 < 20**2 + circle1 = (ic - 135) ** 2 + (ir - 150) ** 2 < 30 ** 2 + circle2 = (ic - 135) ** 2 + (ir - 150) ** 2 < 20 ** 2 image[circle1] = 1 image[circle2] = 0 result = skeletonize(image) - + # there should never be a 2x2 block of foreground pixels in a skeleton mask = np.array([[1, 1], - [1, 1]], np.uint8) + [1, 1]], np.uint8) blocks = correlate(result, mask, mode='constant') assert not numpy.any(blocks == 4) - + + class TestMedialAxis(): def test_00_00_zeros(self): '''Test skeletonize on an array of all zeros''' @@ -101,7 +104,7 @@ class TestMedialAxis(): result = medial_axis(np.zeros((10, 10), bool), np.zeros((10, 10), bool)) assert np.all(result == False) - + def test_01_01_rectangle(self): '''Test skeletonize on a rectangle''' image = np.zeros((9, 15), bool) diff --git a/skimage/morphology/tests/test_watershed.py b/skimage/morphology/tests/test_watershed.py index 82ba6917..1fe8baba 100644 --- a/skimage/morphology/tests/test_watershed.py +++ b/skimage/morphology/tests/test_watershed.py @@ -61,7 +61,7 @@ def diff(a, b): b = np.asarray(b) if (0 in a.shape) and (0 in b.shape): return 0.0 - b[a==0] = 0 + b[a == 0] = 0 if (a.dtype in [np.complex64, np.complex128] or b.dtype in [np.complex64, np.complex128]): a = np.asarray(a, np.complex128) @@ -72,11 +72,13 @@ def diff(a, b): a = a.astype(np.float64) b = np.asarray(b) b = b.astype(np.float64) - t = ((a - b)**2).sum() + t = ((a - b) ** 2).sum() return math.sqrt(t) + class TestWatershed(unittest.TestCase): - eight = np.ones((3, 3),bool) + eight = np.ones((3, 3), bool) + def test_watershed01(self): "watershed 1" data = np.array([[0, 0, 0, 0, 0, 0, 0], @@ -100,7 +102,7 @@ class TestWatershed(unittest.TestCase): [ 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0]], np.int8) - out = watershed(data, markers,self.eight) + out = watershed(data, markers, self.eight) expected = np.array([[-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], @@ -320,7 +322,7 @@ class TestWatershed(unittest.TestCase): [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]]) - mask = (data!=255) + mask = (data != 255) markers = np.zeros(data.shape,int) markers[6, 7] = 1 markers[14, 7] = 2 @@ -331,8 +333,8 @@ class TestWatershed(unittest.TestCase): # size1 = np.sum(out == 1) size2 = np.sum(out == 2) - self.assertTrue(abs(size1-size2) <= 6) - + self.assertTrue(abs(size1 - size2) <= 6) + def test_watershed08(self): "The border pixels + an edge are all the same value" data = np.array([[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], @@ -367,11 +369,11 @@ class TestWatershed(unittest.TestCase): # size1 = np.sum(out == 1) size2 = np.sum(out == 2) - self.assertTrue(abs(size1-size2) <= 6) - + self.assertTrue(abs(size1 - size2) <= 6) + def test_watershed09(self): """Test on an image of reasonable size - + This is here both for timing (does it take forever?) and to ensure that the memory constraints are reasonable """ @@ -383,9 +385,9 @@ class TestWatershed(unittest.TestCase): image[x,y] = 1 markers[x, y] = idx idx += 1 - + image = scipy.ndimage.gaussian_filter(image, 4) - before = time.clock() + before = time.clock() out = watershed(image, markers, self.eight) elapsed = time.clock() - before before = time.clock() @@ -399,7 +401,7 @@ class TestIsLocalMaximum(unittest.TestCase): labels = np.zeros((10, 20), int) result = is_local_maximum(image, labels, np.ones((3, 3), bool)) self.assertTrue(np.all(~ result)) - + def test_01_01_one_point(self): image = np.zeros((10, 20)) labels = np.zeros((10, 20), int) @@ -407,7 +409,7 @@ class TestIsLocalMaximum(unittest.TestCase): labels[5, 5] = 1 result = is_local_maximum(image, labels, np.ones((3, 3), bool)) self.assertTrue(np.all(result == (labels == 1))) - + def test_01_02_adjacent_and_same(self): image = np.zeros((10, 20)) labels = np.zeros((10, 20), int) @@ -415,7 +417,7 @@ class TestIsLocalMaximum(unittest.TestCase): labels[5, 5:6] = 1 result = is_local_maximum(image, labels, np.ones((3, 3), bool)) self.assertTrue(np.all(result == (labels == 1))) - + def test_01_03_adjacent_and_different(self): image = np.zeros((10, 20)) labels = np.zeros((10, 20), int) @@ -427,7 +429,7 @@ class TestIsLocalMaximum(unittest.TestCase): self.assertTrue(np.all(result == expected)) result = is_local_maximum(image, labels) self.assertTrue(np.all(result == expected)) - + def test_01_04_not_adjacent_and_different(self): image = np.zeros((10,20)) labels = np.zeros((10,20), int) @@ -437,7 +439,7 @@ class TestIsLocalMaximum(unittest.TestCase): expected = (labels == 1) result = is_local_maximum(image, labels, np.ones((3,3), bool)) self.assertTrue(np.all(result == expected)) - + def test_01_05_two_objects(self): image = np.zeros((10,20)) labels = np.zeros((10,20), int) @@ -459,26 +461,26 @@ class TestIsLocalMaximum(unittest.TestCase): expected = (labels > 0) result = is_local_maximum(image, labels, np.ones((3,3), bool)) self.assertTrue(np.all(result == expected)) - + def test_02_01_four_quadrants(self): np.random.seed(21) image = np.random.uniform(size=(40,60)) i,j = np.mgrid[0:40,0:60] labels = 1 + (i >= 20) + (j >= 30) * 2 i,j = np.mgrid[-3:4,-3:4] - footprint = (i*i + j*j <=9) + footprint = (i * i + j * j <= 9) expected = np.zeros(image.shape, float) for imin, imax in ((0, 20), (20, 40)): for jmin, jmax in ((0, 30), (30, 60)): expected[imin:imax,jmin:jmax] = scipy.ndimage.maximum_filter( - image[imin:imax, jmin:jmax], footprint = footprint) + image[imin:imax, jmin:jmax], footprint=footprint) expected = (expected == image) result = is_local_maximum(image, labels, footprint) self.assertTrue(np.all(result == expected)) - + def test_03_01_disk_1(self): '''regression test of img-1194, footprint = [1] - + Test is_local_maximum when every point is a local maximum ''' np.random.seed(31) @@ -488,6 +490,6 @@ class TestIsLocalMaximum(unittest.TestCase): self.assertTrue(np.all(result)) result = is_local_maximum(image, footprint=footprint) self.assertTrue(np.all(result)) - + if __name__ == "__main__": np.testing.run_module_suite() diff --git a/skimage/morphology/watershed.py b/skimage/morphology/watershed.py index 1966a1a8..71eaf113 100644 --- a/skimage/morphology/watershed.py +++ b/skimage/morphology/watershed.py @@ -32,6 +32,7 @@ from ..filter import rank_order from . import _watershed import warnings + def watershed(image, markers, connectivity=None, offset=None, mask=None): """ Return a matrix labeled using the watershed segmentation algorithm @@ -87,8 +88,8 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): This implementation converts all arguments to specific, lowest common denominator types, then passes these to a C algorithm. - - Markers can be determined manually, or automatically using for example + + Markers can be determined manually, or automatically using for example the local minima of the gradient of the image, or the local maxima of the distance function to the background for separating overlapping objects (see example). @@ -138,7 +139,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): # offset = np.array(c_connectivity.shape) // 2 - # pad the image, markers, and mask so that we can use the mask to + # pad the image, markers, and mask so that we can use the mask to # keep from running off the edges pads = offset @@ -165,10 +166,10 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): if mask != None: c_mask = np.ascontiguousarray(mask, dtype=bool) if c_mask.ndim != c_markers.ndim: - raise ValueError, "mask must have same # of dimensions as image" + raise ValueError("mask must have same # of dimensions as image") if c_markers.shape != c_mask.shape: - raise ValueError, "mask must have same shape as image" - c_markers[np.logical_not(mask)]=0 + raise ValueError("mask must have same shape as image") + c_markers[np.logical_not(mask)] = 0 else: c_mask = None c_output = c_markers.copy() @@ -190,8 +191,8 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): ignore = True for j in range(len(c_connectivity.shape)): elems = c_image.shape[j] - idx = (i // multiplier) % c_connectivity.shape[j] - off = idx - offset[j] + idx = (i // multiplier) % c_connectivity.shape[j] + off = idx - offset[j] if off: ignore = False offs.append(off) @@ -214,10 +215,10 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): else: c_mask = c_mask.astype(np.int8).flatten() _watershed.watershed(c_image.flatten(), - pq, age, c, - c_image.ndim, + pq, age, c, + c_image.ndim, c_mask, - np.array(c_image.shape,np.int32), + np.array(c_image.shape, np.int32), c_output) c_output = c_output.reshape(c_image.shape)[[slice(1, -1, None)] * image.ndim] @@ -235,13 +236,13 @@ def is_local_maximum(image, labels=None, footprint=None): ---------- image: ndarray (2-D, 3-D, ...) intensity image - - labels: ndarray, optional + + labels: ndarray, optional find maxima only within labels. Zero is reserved for background. - + footprint: ndarray of bools, optional binary mask indicating the neighborhood to be examined - `footprint` must be a matrix with odd dimensions, the center is taken + `footprint` must be a matrix with odd dimensions, the center is taken to be the point in question. Returns @@ -289,7 +290,7 @@ def is_local_maximum(image, labels=None, footprint=None): footprint = np.ones([3] * image.ndim, dtype=np.uint8) assert((np.all(footprint.shape) & 1) == 1) footprint = (footprint != 0) - footprint_extent = (np.array(footprint.shape)-1) // 2 + footprint_extent = (np.array(footprint.shape) - 1) // 2 if np.all(footprint_extent == 0): return labels > 0 result = (labels > 0).copy() @@ -297,17 +298,17 @@ def is_local_maximum(image, labels=None, footprint=None): # Create a labels matrix with zeros at the borders that might be # hit by the footprint. # - big_labels = np.zeros(np.array(labels.shape) + footprint_extent*2, + big_labels = np.zeros(np.array(labels.shape) + footprint_extent * 2, labels.dtype) - big_labels[[slice(fe,-fe) for fe in footprint_extent]] = labels + big_labels[[slice(fe, -fe) for fe in footprint_extent]] = labels # # Find the relative indexes of each footprint element # image_strides = np.array(image.strides) // image.dtype.itemsize big_strides = np.array(big_labels.strides) // big_labels.dtype.itemsize result_strides = np.array(result.strides) // result.dtype.itemsize - footprint_offsets = np.mgrid[[slice(-fe,fe+1) for fe in footprint_extent]] - + footprint_offsets = np.mgrid[[slice(-fe, fe + 1) for fe in footprint_extent]] + fp_image_offsets = np.sum(image_strides[:, np.newaxis] * footprint_offsets[:, footprint], 0) fp_big_offsets = np.sum(big_strides[:, np.newaxis] * @@ -315,9 +316,9 @@ def is_local_maximum(image, labels=None, footprint=None): # # Get the index of each labeled pixel in the image and big_labels arrays # - indexes = np.mgrid[[slice(0,x) for x in labels.shape]][:, labels > 0] + indexes = np.mgrid[[slice(0, x) for x in labels.shape]][:, labels > 0] image_indexes = np.sum(image_strides[:, np.newaxis] * indexes, 0) - big_indexes = np.sum(big_strides[:, np.newaxis] * + big_indexes = np.sum(big_strides[:, np.newaxis] * (indexes + footprint_extent[:, np.newaxis]), 0) result_indexes = np.sum(result_strides[:, np.newaxis] * indexes, 0) # @@ -335,18 +336,15 @@ def is_local_maximum(image, labels=None, footprint=None): same_label = (big_labels_raveled[big_indexes + fp_big_offset] == big_labels_raveled[big_indexes]) less_than = (image_raveled[image_indexes[same_label]] < - image_raveled[image_indexes[same_label]+ fp_image_offset]) + image_raveled[image_indexes[same_label] + fp_image_offset]) result_raveled[result_indexes[same_label][less_than]] = False - + return result - # ---------------------- deprecated ------------------------------ -# Deprecate slower pure-Python code, that we keep only for +# Deprecate slower pure-Python code, that we keep only for # pedagogical purposes - - def __heapify_markers(markers, image): """Create a priority queue heap with the markers on it""" stride = np.array(image.strides) // image.itemsize @@ -354,24 +352,25 @@ def __heapify_markers(markers, image): ncoords = coords.shape[0] if ncoords > 0: pixels = image[markers != 0] - age = np.arange(ncoords) + age = np.arange(ncoords) offset = np.zeros(coords.shape[0], int) for i in range(image.ndim): offset = offset + stride[i] * coords[:, i] pq = np.column_stack((pixels, age, offset, coords)) # pixels = top priority, age=second - ordering = np.lexsort((age, pixels)) + ordering = np.lexsort((age, pixels)) pq = pq[ordering, :] else: pq = np.zeros((0, markers.ndim + 3), int) return (pq, ncoords) - + + def _slow_watershed(image, markers, connectivity=8, mask=None): """Return a matrix labeled using the watershed algorithm - + Use the `watershed` function for a faster execution. This pure Python function is solely for pedagogical purposes. - + Parameters ---------- image: 2-d ndarray of integers @@ -380,16 +379,16 @@ def _slow_watershed(image, markers, connectivity=8, mask=None): markers: 2-d ndarray of integers a two-dimensional matrix marking the basins with the values to be assigned in the label matrix. Zero means not a marker. - connectivity: {4, 8}, optional + connectivity: {4, 8}, optional either 4 for four-connected or 8 (default) for eight-connected - mask: 2-d ndarray of bools, optional + mask: 2-d ndarray of bools, optional don't label points in the mask - Returns + Returns ------- out: ndarray A labeled matrix of the same type and shape as markers - + Notes ----- @@ -409,20 +408,20 @@ def _slow_watershed(image, markers, connectivity=8, mask=None): This implementation converts all arguments to specific, lowest common denominator types, then passes these to a C algorithm. - - Markers can be determined manually, or automatically using for example + + Markers can be determined manually, or automatically using for example the local minima of the gradient of the image, or the local maxima of the distance function to the background for separating overlapping objects. """ if connectivity not in (4, 8): raise ValueError("Connectivity was %d: it should be either \ - four or eight" %(connectivity)) - + four or eight" % (connectivity)) + image = np.array(image) markers = np.array(markers) labels = markers.copy() - max_x = markers.shape[0] - max_y = markers.shape[1] + max_x = markers.shape[0] + max_y = markers.shape[1] if connectivity == 4: connect_increments = ((1, 0), (0, 1), (-1, 0), (0, -1)) else: diff --git a/skimage/scripts/skivi b/skimage/scripts/skivi index 4469f015..e3108f64 100755 --- a/skimage/scripts/skivi +++ b/skimage/scripts/skivi @@ -3,4 +3,3 @@ if __name__ == "__main__": from skimage.scripts import skivi skivi.main() - diff --git a/skimage/scripts/skivi.py b/skimage/scripts/skivi.py index 610013cc..2e907bbe 100644 --- a/skimage/scripts/skivi.py +++ b/skimage/scripts/skivi.py @@ -1,4 +1,6 @@ """skimage viewer""" + + def main(): import skimage.io as io import sys @@ -10,4 +12,3 @@ def main(): io.use_plugin('qt') io.imshow(io.imread(sys.argv[1]), fancy=True) io.show() - diff --git a/skimage/setup.py b/skimage/setup.py index c26014f8..0afc5bc9 100644 --- a/skimage/setup.py +++ b/skimage/setup.py @@ -1,21 +1,22 @@ import os + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration config = Configuration('skimage', parent_package, top_path) - config.add_subpackage('graph') - config.add_subpackage('io') - config.add_subpackage('morphology') - config.add_subpackage('filter') - config.add_subpackage('transform') - config.add_subpackage('data') - config.add_subpackage('util') config.add_subpackage('color') + config.add_subpackage('data') config.add_subpackage('draw') config.add_subpackage('feature') + config.add_subpackage('filter') + config.add_subpackage('graph') + config.add_subpackage('io') config.add_subpackage('measure') + config.add_subpackage('morphology') + config.add_subpackage('transform') + config.add_subpackage('util') def add_test_directories(arg, dirname, fnames): if dirname.split(os.path.sep)[-1] == 'tests': @@ -37,4 +38,3 @@ if __name__ == "__main__": config = configuration(top_path='').todict() setup(**config) - diff --git a/skimage/transform/_warp_zoo.py b/skimage/transform/_warp_zoo.py index 6fd92c44..c2e8b4df 100644 --- a/skimage/transform/_warp_zoo.py +++ b/skimage/transform/_warp_zoo.py @@ -7,7 +7,7 @@ from ._warp import warp def _swirl_mapping(xy, center, rotation, strength, radius): x, y = xy.T x0, y0 = center - rho = np.sqrt((x - x0)**2 + (y - y0)**2) + rho = np.sqrt((x - x0) ** 2 + (y - y0) ** 2) # Ensure that the transformation decays to approximately 1/1000-th # within the specified radius. @@ -22,6 +22,7 @@ def _swirl_mapping(xy, center, rotation, strength, radius): return xy + def swirl(image, center=None, strength=1, radius=100, rotation=0, output_shape=None, order=1, mode='constant', cval=0): """Perform a swirl transformation. diff --git a/skimage/transform/finite_radon_transform.py b/skimage/transform/finite_radon_transform.py index 03dd0152..13007dce 100644 --- a/skimage/transform/finite_radon_transform.py +++ b/skimage/transform/finite_radon_transform.py @@ -9,6 +9,7 @@ __docformat__ = "restructuredtext en" import numpy as np from numpy import roll, newaxis + def frt2(a): """Compute the 2-dimensional finite radon transform (FRT) for an n x n integer array. @@ -65,7 +66,7 @@ def frt2(a): ai = a.copy() n = ai.shape[0] - f = np.empty((n+1, n), np.uint32) + f = np.empty((n + 1, n), np.uint32) f[0] = ai.sum(axis=0) for m in range(1, n): # Roll the pth row of ai left by p places @@ -125,7 +126,7 @@ def ifrt2(a): and Electron Physics, 139 (2006) """ - if a.ndim != 2 or a.shape[0] != a.shape[1]+1: + if a.ndim != 2 or a.shape[0] != a.shape[1] + 1: raise ValueError("Input must be an (n+1) row x n column, 2-D array") ai = a.copy()[:-1] @@ -138,5 +139,5 @@ def ifrt2(a): ai[row] = roll(ai[row], row) f[m] = ai.sum(axis=0) f += a[-1][newaxis].T - f = (f - ai[0].sum())/n + f = (f - ai[0].sum()) / n return f diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index 5bbe3b6c..e4cb5b5c 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -3,7 +3,8 @@ __all__ = ['hough', 'probabilistic_hough'] from itertools import izip as zip import numpy as np -from ._hough_transform import _probabilistic_hough +from ._hough_transform import _probabilistic_hough + def _hough(img, theta=None): if img.ndim != 2: @@ -68,28 +69,28 @@ def probabilistic_hough(img, threshold=10, line_length=50, line_gap=10, theta=No img : (M, N) ndarray Input image with nonzero values representing edges. threshold : int - Threshold + Threshold line_length : int, optional (default 50) Minimum accepted length of detected lines. Increase the parameter to extract longer lines. line_gap : int, optional, (default 10) - Maximum gap between pixels to still form a line. + Maximum gap between pixels to still form a line. Increase the parameter to merge broken lines more aggresively. theta : 1D ndarray, dtype=double, optional, default (-pi/2 .. pi/2) Angles at which to compute the transform, in radians. - + Returns ------- lines : list - List of lines identified, lines in format ((x0, y0), (x1, y0)), indicating + List of lines identified, lines in format ((x0, y0), (x1, y0)), indicating line start and end. References ---------- - .. [1] C. Galamhos, J. Matas and J. Kittler,"Progressive probabilistic Hough - transform for line detection", in IEEE Computer Society Conference on - Computer Vision and Pattern Recognition, 1999. - """ + .. [1] C. Galamhos, J. Matas and J. Kittler,"Progressive probabilistic Hough + transform for line detection", in IEEE Computer Society Conference on + Computer Vision and Pattern Recognition, 1999. + """ return _probabilistic_hough(img, threshold, line_length, line_gap, theta) @@ -141,5 +142,3 @@ def hough(img, theta=None): """ return _hough(img, theta) - - diff --git a/skimage/transform/integral.py b/skimage/transform/integral.py index 440d10e6..c34b6a19 100644 --- a/skimage/transform/integral.py +++ b/skimage/transform/integral.py @@ -26,6 +26,7 @@ def integral_image(x): """ return x.cumsum(1).cumsum(0) + def integrate(ii, r0, c0, r1, c1): """Use an integral image to integrate over a given window. diff --git a/skimage/transform/project.py b/skimage/transform/project.py index 030e86a9..a140e03e 100644 --- a/skimage/transform/project.py +++ b/skimage/transform/project.py @@ -10,6 +10,7 @@ __all__ = ['homography'] eps = np.finfo(float).eps + def homography(image, H, output_shape=None, order=1, mode='constant', cval=0.): """Perform a projective transformation (homography) on an image. @@ -86,7 +87,7 @@ def homography(image, H, output_shape=None, order=1, [ 0, 0, 0, 255, 0], [ 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0]], dtype=uint8) - + """ if image.ndim < 2: raise ValueError("Input must have more than 1 dimension.") @@ -120,13 +121,13 @@ def homography(image, H, output_shape=None, order=1, tf_coords[..., :2] /= tf_coords[..., 2, np.newaxis] # y-coordinate mapping - _stackcopy(coords[0,...], tf_coords[...,1]) + _stackcopy(coords[0, ...], tf_coords[..., 1]) # x-coordinate mapping - _stackcopy(coords[1,...], tf_coords[...,0]) + _stackcopy(coords[1, ...], tf_coords[..., 0]) # colour-coordinate mapping - coords[2,...] = range(bands) + coords[2, ...] = range(bands) # Prefilter not necessary for order 1 interpolation prefilter = order > 1 diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 5da31a72..2480c7fc 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -66,7 +66,6 @@ def radon(image, theta=None): [0, 1, dh], [0, 0, 1]]) - def build_rotation(theta): T = -np.deg2rad(theta) @@ -80,7 +79,7 @@ def radon(image, theta=None): rotated = homography(padded_image, build_rotation(-theta[i])) - out[:,i] = rotated.sum(0)[::-1] + out[:, i] = rotated.sum(0)[::-1] return out @@ -151,11 +150,11 @@ def iradon(radon_image, theta=None, output_size=None, elif filter == "shepp-logan": f[1:] = f[1:] * np.sin(w[1:] / 2) / (w[1:] / 2) elif filter == "cosine": - f[1:] = f[1:] * np.cos(w[1:] / 2) + f[1:] = f[1:] * np.cos(w[1:] / 2) elif filter == "hamming": - f[1:] = f[1:] * (0.54 + 0.46 * np.cos(w[1:])) + f[1:] = f[1:] * (0.54 + 0.46 * np.cos(w[1:])) elif filter == "hann": - f[1:] = f[1:] * (1 + np.cos(w[1:])) / 2 + f[1:] = f[1:] * (1 + np.cos(w[1:])) / 2 elif filter == None: f[1:] = 1 else: @@ -184,13 +183,13 @@ def iradon(radon_image, theta=None, output_size=None, ((((k > 0) & (k < n)) * k) - 1).astype(np.int), i] elif interpolation == "linear": for i in range(len(theta)): - t = xpr*np.sin(th[i]) - ypr*np.cos(th[i]) - a = np.floor(t) - b = mid_index + a - b0 = ((((b + 1 > 0) & (b + 1 < n)) * (b + 1)) - 1).astype(np.int) - b1 = ((((b > 0) & (b < n)) * b) - 1).astype(np.int) - reconstructed += (t - a) * radon_filtered[b0, i] + \ - (a - t + 1) * radon_filtered[b1, i] + t = xpr * np.sin(th[i]) - ypr * np.cos(th[i]) + a = np.floor(t) + b = mid_index + a + b0 = ((((b + 1 > 0) & (b + 1 < n)) * (b + 1)) - 1).astype(np.int) + b1 = ((((b > 0) & (b < n)) * b) - 1).astype(np.int) + reconstructed += (t - a) * radon_filtered[b0, i] + \ + (a - t + 1) * radon_filtered[b1, i] else: raise ValueError("Unknown interpolation: %s" % interpolation) diff --git a/skimage/transform/setup.py b/skimage/transform/setup.py index 274dad94..4ecb6ba4 100644 --- a/skimage/transform/setup.py +++ b/skimage/transform/setup.py @@ -6,6 +6,7 @@ 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 @@ -21,16 +22,15 @@ def configuration(parent_package='', top_path=None): config.add_extension('_project', sources=['_project.c'], include_dirs=[get_numpy_include_dirs()]) - return config if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'Scikits-image Developers', - author = 'Scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Transforms', - url = 'https://github.com/scikits-image/scikits-image', - license = 'SciPy License (BSD Style)', + setup(maintainer='Scikits-image Developers', + author='Scikits-image Developers', + maintainer_email='scikits-image@googlegroups.com', + description='Transforms', + url='https://github.com/scikits-image/scikits-image', + license='SciPy License (BSD Style)', **(configuration(top_path='').todict()) ) diff --git a/skimage/transform/tests/test_finite_radon_transform.py b/skimage/transform/tests/test_finite_radon_transform.py index d8504477..b5fcf43a 100644 --- a/skimage/transform/tests/test_finite_radon_transform.py +++ b/skimage/transform/tests/test_finite_radon_transform.py @@ -3,6 +3,7 @@ from numpy.testing import * from skimage.transform import * + def test_frt(): SIZE = 59 try: @@ -15,4 +16,4 @@ def test_frt(): L = np.tri(SIZE, dtype=np.int32) + np.tri(SIZE, dtype=np.int32)[::-1] f = frt2(L) fi = ifrt2(f) - assert len(np.nonzero(L-fi)[0]) == 0 + assert len(np.nonzero(L - fi)[0]) == 0 diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index b75291e8..7472ebe0 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -5,6 +5,7 @@ import skimage.transform as tf import skimage.transform.hough_transform as ht from skimage.transform import probabilistic_hough + def append_desc(func, description): """Append the test function ``func`` and append ``description`` to its name. @@ -15,6 +16,7 @@ def append_desc(func, description): from skimage.transform import * + def test_hough(): # Generate a test image img = np.zeros((100, 100), dtype=int) @@ -39,6 +41,7 @@ def test_hough_angles(): assert_equal(len(angles), 10) + def test_py_hough(): ht._hough, fast_hough = ht._py_hough, ht._hough @@ -47,6 +50,7 @@ def test_py_hough(): tf._hough = fast_hough + def test_probabilistic_hough(): # Generate a test image img = np.zeros((100, 100), dtype=int) diff --git a/skimage/transform/tests/test_integral.py b/skimage/transform/tests/test_integral.py index b8905d92..d443189c 100644 --- a/skimage/transform/tests/test_integral.py +++ b/skimage/transform/tests/test_integral.py @@ -6,19 +6,22 @@ from skimage.transform import integral_image, integrate x = (np.random.random((50, 50)) * 255).astype(np.uint8) s = integral_image(x) + def test_validity(): - y = np.arange(12).reshape((4,3)) + y = np.arange(12).reshape((4, 3)) y = (np.random.random((50, 50)) * 255).astype(np.uint8) assert_equal(integral_image(y)[-1, -1], y.sum()) + def test_basic(): assert_equal(x[12:24, 10:20].sum(), integrate(s, 12, 10, 23, 19)) assert_equal(x[:20, :20].sum(), integrate(s, 0, 0, 19, 19)) assert_equal(x[:20, 10:20].sum(), integrate(s, 0, 10, 19, 19)) assert_equal(x[10:20, :20].sum(), integrate(s, 10, 0, 19, 19)) + def test_single(): assert_equal(x[0, 0], integrate(s, 0, 0, 0, 0)) assert_equal(x[10, 10], integrate(s, 10, 10, 10, 10)) diff --git a/skimage/transform/tests/test_project.py b/skimage/transform/tests/test_project.py index 3446c9a5..011c6e9c 100644 --- a/skimage/transform/tests/test_project.py +++ b/skimage/transform/tests/test_project.py @@ -12,7 +12,8 @@ def test_stackcopy(): y = np.eye(3, 3) _stackcopy(x, y) for i in range(layers): - assert_array_almost_equal(x[...,i], y) + assert_array_almost_equal(x[..., i], y) + def test_homography(): x = np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1 @@ -23,10 +24,11 @@ def test_homography(): x90 = homography(x, M, order=1) assert_array_almost_equal(x90, np.rot90(x)) + def test_fast_homography(): img = rgb2gray(data.lena()).astype(np.uint8) img = img[:, :100] - + theta = np.deg2rad(30) scale = 0.5 tx, ty = 50, 50 @@ -53,7 +55,7 @@ def test_fast_homography(): d = np.mean(np.abs(p0 - p1)) assert d < 0.2 - + if __name__ == "__main__": from numpy.testing import run_module_suite diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index b52ff69b..0c783651 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -4,12 +4,14 @@ import numpy as np from numpy.testing import * from skimage.transform import * + def rescale(x): x = x.astype(float) x -= x.min() x /= x.max() return x + def test_radon_iradon(): size = 100 debug = False diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index b039123c..6383c53f 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -94,7 +94,7 @@ def convert(image, dtype, force_copy=False, uniform=False): def _dtype2(kind, bits, itemsize=1): # Return dtype of `kind` that can store a `bits` wide unsigned int c = lambda x, y: x <= y if kind == 'u' else x < y - s = next(i for i in (itemsize, ) + (2, 4, 8) if c(bits, i*8)) + s = next(i for i in (itemsize, ) + (2, 4, 8) if c(bits, i * 8)) return np.dtype(kind + str(s)) def _scale(a, n, m, copy=True): @@ -109,21 +109,21 @@ def convert(image, dtype, force_copy=False, uniform=False): prec_loss() if copy: b = np.empty(a.shape, _dtype2(kind, m)) - np.floor_divide(a, 2**(n - m), out=b, dtype=a.dtype, + np.floor_divide(a, 2 ** (n - m), out=b, dtype=a.dtype, casting='unsafe') return b else: - a //= 2**(n - m) + a //= 2 ** (n - m) return a elif m % n == 0: # exact upscale to a multiple of n bits if copy: b = np.empty(a.shape, _dtype2(kind, m)) - np.multiply(a, (2**m - 1) // (2**n - 1), out=b, dtype=b.dtype) + np.multiply(a, (2 ** m - 1) // (2 ** n - 1), out=b, dtype=b.dtype) return b else: a = np.array(a, _dtype2(kind, m, a.dtype.itemsize), copy=False) - a *= (2**m - 1) // (2**n - 1) + a *= (2 ** m - 1) // (2 ** n - 1) return a else: # upscale to a multiple of n bits, @@ -132,13 +132,13 @@ def convert(image, dtype, force_copy=False, uniform=False): o = (m // n + 1) * n if copy: b = np.empty(a.shape, _dtype2(kind, o)) - np.multiply(a, (2**o - 1) // (2**n - 1), out=b, dtype=b.dtype) - b //= 2**(o - m) + np.multiply(a, (2 ** o - 1) // (2 ** n - 1), out=b, dtype=b.dtype) + b //= 2 ** (o - m) return b else: a = np.array(a, _dtype2(kind, o, a.dtype.itemsize), copy=False) - a *= (2**o - 1) // (2**n - 1) - a //= 2**(o - m) + a *= (2 ** o - 1) // (2 ** n - 1) + a //= 2 ** (o - m) return a kind = dtypeobj.kind @@ -205,26 +205,26 @@ def convert(image, dtype, force_copy=False, uniform=False): if kind_in == 'u': if kind == 'i': # unsigned integer -> signed integer - image = _scale(image, 8*itemsize_in, 8*itemsize-1) + image = _scale(image, 8 * itemsize_in, 8 * itemsize - 1) return image.view(dtype) else: # unsigned integer -> unsigned integer - return _scale(image, 8*itemsize_in, 8*itemsize) + return _scale(image, 8 * itemsize_in, 8 * itemsize) if kind == 'u': # signed integer -> unsigned integer sign_loss() - image = _scale(image, 8*itemsize_in-1, 8*itemsize) + image = _scale(image, 8 * itemsize_in - 1, 8 * itemsize) result = np.empty(image.shape, dtype) np.maximum(image, 0, out=result, dtype=image.dtype, casting='unsafe') return result # signed integer -> signed integer if itemsize_in > itemsize: - return _scale(image, 8*itemsize_in-1, 8*itemsize-1) - image = image.astype(_dtype2('i', itemsize*8)) + return _scale(image, 8 * itemsize_in - 1, 8 * itemsize - 1) + image = image.astype(_dtype2('i', itemsize * 8)) image -= imin_in - image = _scale(image, 8*itemsize_in, 8*itemsize, copy=False) + image = _scale(image, 8 * itemsize_in, 8 * itemsize, copy=False) image += imin return dtype(image) diff --git a/skimage/util/tests/test_dtype.py b/skimage/util/tests/test_dtype.py index 2ef0044f..816d5d4c 100644 --- a/skimage/util/tests/test_dtype.py +++ b/skimage/util/tests/test_dtype.py @@ -12,11 +12,13 @@ dtype_range = {np.uint8: (0, 255), np.float32: (-1.0, 1.0), np.float64: (-1.0, 1.0)} + def _verify_range(msg, x, vmin, vmax, dtype): assert_equal(x[0], vmin) assert_equal(x[-1], vmax) assert x.dtype == dtype + def test_range(): for dtype in dtype_range: imin, imax = dtype_range[dtype] From 9f34c84f1a6d4599549db66583e97c37a60c6728 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 15 Jun 2012 21:15:29 +0200 Subject: [PATCH 006/648] COSMIT don't let pep8 make your matrices ugly. --- skimage/color/tests/test_colorconv.py | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index b053d470..84c921b8 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -77,14 +77,14 @@ class TestColorconv(TestCase): # RGB to XYZ def test_rgb2xyz_conversion(self): - gt = np.array([[[0.950456, 1., 1.088754], - [0.538003, 0.787329, 1.06942], - [0.592876, 0.28484, 0.969561], - [0.180423, 0.072169, 0.950227]], - [[0.770033, 0.927831, 0.138527], - [0.35758, 0.71516, 0.119193], - [0.412453, 0.212671, 0.019334], - [0., 0., 0.]]]) + gt = np.array([[[0.950456, 1. , 1.088754], + [0.538003, 0.787329, 1.06942 ], + [0.592876, 0.28484 , 0.969561], + [0.180423, 0.072169, 0.950227]], + [[0.770033, 0.927831, 0.138527], + [0.35758 , 0.71516 , 0.119193], + [0.412453, 0.212671, 0.019334], + [0. , 0. , 0. ]]]) assert_almost_equal(rgb2xyz(self.colbars_array), gt) # stop repeating the "raises" checks for all other functions that are @@ -103,14 +103,14 @@ class TestColorconv(TestCase): # RGB to RGB CIE def test_rgb2rgbcie_conversion(self): - gt = np.array([[[0.1488856, 0.18288098, 0.19277574], - [0.01163224, 0.16649536, 0.18948516], - [0.12259182, 0.03308008, 0.17298223], + gt = np.array([[[ 0.1488856 , 0.18288098, 0.19277574], + [ 0.01163224, 0.16649536, 0.18948516], + [ 0.12259182, 0.03308008, 0.17298223], [-0.01466154, 0.01669446, 0.16969164]], - [[0.16354714, 0.16618652, 0.0230841], - [0.02629378, 0.1498009, 0.01979351], - [0.13725336, 0.01638562, 0.00329059], - [0., 0., 0.]]]) + [[ 0.16354714, 0.16618652, 0.0230841 ], + [ 0.02629378, 0.1498009 , 0.01979351], + [ 0.13725336, 0.01638562, 0.00329059], + [ 0. , 0. , 0. ]]]) assert_almost_equal(rgb2rgbcie(self.colbars_array), gt) # RGB CIE to RGB From ee0fd867daecc442d022c4a1812af4da104cf4e0 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 22 Jun 2012 23:40:42 +0200 Subject: [PATCH 007/648] COSMIT fix ugly line breaks. --- skimage/filter/tests/test_tv_denoise.py | 4 ++-- skimage/filter/tv_denoise.py | 4 ++-- skimage/graph/tests/test_mcp.py | 2 +- skimage/io/_plugins/q_color_mixer.py | 7 +++---- skimage/io/_plugins/util.py | 5 ++--- skimage/io/collection.py | 8 ++++---- skimage/io/tests/test_freeimage.py | 4 ++-- skimage/io/tests/test_plugin_util.py | 3 +-- skimage/io/video.py | 24 ++++++++++++------------ 9 files changed, 29 insertions(+), 32 deletions(-) diff --git a/skimage/filter/tests/test_tv_denoise.py b/skimage/filter/tests/test_tv_denoise.py index 323c40cb..27db4894 100644 --- a/skimage/filter/tests/test_tv_denoise.py +++ b/skimage/filter/tests/test_tv_denoise.py @@ -27,8 +27,8 @@ class TestTvDenoise(): grad_denoised = ndimage.morphological_gradient( denoised_lena, size=((3, 3))) # test if the total variation has decreased - assert np.sqrt( - (grad_denoised ** 2).sum()) < np.sqrt((grad ** 2).sum()) / 2 + assert (np.sqrt((grad_denoised ** 2).sum()) + < np.sqrt((grad ** 2).sum()) / 2) denoised_lena_int = filter.tv_denoise(img_as_uint(lena), weight=60.0, keep_type=True) assert denoised_lena_int.dtype is np.dtype('uint16') diff --git a/skimage/filter/tv_denoise.py b/skimage/filter/tv_denoise.py index db45d63e..37abdbb5 100644 --- a/skimage/filter/tv_denoise.py +++ b/skimage/filter/tv_denoise.py @@ -252,8 +252,8 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): elif im.ndim == 3: out = _tv_denoise_3d(im, weight, eps, n_iter_max) else: - raise ValueError( - 'only 2-d and 3-d images may be denoised with this function') + raise ValueError('only 2-d and 3-d images may be denoised with this' + 'function') if keep_type: return out.astype(im_type) else: diff --git a/skimage/graph/tests/test_mcp.py b/skimage/graph/tests/test_mcp.py index f983d87f..e7b75cdc 100644 --- a/skimage/graph/tests/test_mcp.py +++ b/skimage/graph/tests/test_mcp.py @@ -145,7 +145,7 @@ def _test_random(shape): starts = [[0] * len(shape), [-1] * len(shape), (np.random.random(len(shape)) * shape).astype(int)] ends = [(np.random.random(len(shape)) * shape).astype(int) - for i in range(4)] + for i in range(4)] m = mcp.MCP(a, fully_connected=True) costs, offsets = m.find_costs(starts) for point in [(np.random.random(len(shape)) * shape).astype(int) diff --git a/skimage/io/_plugins/q_color_mixer.py b/skimage/io/_plugins/q_color_mixer.py index 5b6a461a..3fe9e29c 100644 --- a/skimage/io/_plugins/q_color_mixer.py +++ b/skimage/io/_plugins/q_color_mixer.py @@ -1,7 +1,6 @@ # the module for the qt color_mixer plugin from PyQt4 import QtGui, QtCore -from PyQt4.QtGui import (QWidget, QStackedWidget, QSlider, QVBoxLayout, - QGridLayout, QLabel) +from PyQt4.QtGui import (QWidget, QStackedWidget, QSlider, QGridLayout, QLabel) from util import ColorMixer @@ -36,8 +35,8 @@ class IntelligentSlider(QWidget): self.name_label.setAlignment(QtCore.Qt.AlignCenter) self.value_label = QLabel() - self.value_label.setText( - '%2.2f' % (self.slider.value() * self.a + self.b)) + self.value_label.setText('%2.2f' % (self.slider.value() * self.a + + self.b)) self.value_label.setAlignment(QtCore.Qt.AlignCenter) self.layout = QGridLayout(self) diff --git a/skimage/io/_plugins/util.py b/skimage/io/_plugins/util.py index f40a04a8..7c095e82 100644 --- a/skimage/io/_plugins/util.py +++ b/skimage/io/_plugins/util.py @@ -64,8 +64,8 @@ class WindowManager(object): self._gui_lock = False self._guikit = '' else: - raise RuntimeError( - 'Only the toolkit that owns the lock may release it') + raise RuntimeError('Only the toolkit that owns the lock may' + 'release it') def add_window(self, win): self._check_locked() @@ -191,7 +191,6 @@ class ImgThread(threading.Thread): class ThreadDispatch(object): def __init__(self, img, stateimg, func, *args): - width = img.shape[1] height = img.shape[0] self.cores = CPU_COUNT self.threads = [] diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 973d9b30..e698b52b 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -118,8 +118,8 @@ class MultiImage(object): if -numframes <= n < numframes: n = n % numframes else: - raise IndexError( - "There are only %s frames in the image" % numframes) + raise IndexError("There are only %s frames in the image" + % numframes) if self.conserve_memory: if not self._cached == n: @@ -280,8 +280,8 @@ class ImageCollection(object): if -num <= n < num: n = n % num else: - raise IndexError( - "There are only %s images in the collection" % num) + raise IndexError("There are only %s images in the collection" + % num) return n def __iter__(self): diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index e58c18cc..565f37bd 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -85,8 +85,8 @@ def test_metadata(): assert meta[('EXIF_MAIN', 'Orientation')] == 1 assert meta[('EXIF_MAIN', 'Software')].startswith('ImageMagick') - meta = fi.read_multipage_metadata( - os.path.join(si.data_dir, 'multipage.tif')) + meta = fi.read_multipage_metadata(os.path.join(si.data_dir, + 'multipage.tif')) assert len(meta) == 2 assert meta[0][('EXIF_MAIN', 'Orientation')] == 1 assert meta[1][('EXIF_MAIN', 'Software')].startswith('ImageMagick') diff --git a/skimage/io/tests/test_plugin_util.py b/skimage/io/tests/test_plugin_util.py index c302b3f2..09b758d8 100644 --- a/skimage/io/tests/test_plugin_util.py +++ b/skimage/io/tests/test_plugin_util.py @@ -13,8 +13,7 @@ class TestPrepareForDisplay: assert x.dtype == np.dtype(np.uint8) def test_grey(self): - x = prepare_for_display( - np.arange(12, dtype=float).reshape((4, 3)) / 11.) + x = prepare_for_display(np.arange(12, dtype=float).reshape((4, 3)) / 11) assert_array_equal(x[..., 0], x[..., 2]) assert x[0, 0, 0] == 0 assert x[3, 2, 0] == 255 diff --git a/skimage/io/video.py b/skimage/io/video.py index 92d46e9c..6cb1a8e9 100644 --- a/skimage/io/video.py +++ b/skimage/io/video.py @@ -1,6 +1,5 @@ import numpy as np import os -import time from skimage.io import ImageCollection try: @@ -57,8 +56,8 @@ class CvVideo(object): else: cv.Resize(img, cv.fromarray(img_mat)) # opencv stores images in BGR format - cv.CvtColor( - cv.fromarray(img_mat), cv.fromarray(img_mat), cv.CV_BGR2RGB) + cv.CvtColor(cv.fromarray(img_mat), cv.fromarray(img_mat), + cv.CV_BGR2RGB) return img_mat def seek_frame(self, frame_number): @@ -70,8 +69,8 @@ class CvVideo(object): frame_number : int Frame position """ - cv.SetCaptureProperty( - self.capture, cv.CV_CAP_PROP_POS_FRAMES, frame_number) + cv.SetCaptureProperty(self.capture, cv.CV_CAP_PROP_POS_FRAMES, + frame_number) def seek_time(self, milliseconds): """ @@ -82,8 +81,8 @@ class CvVideo(object): milliseconds : int Time position """ - cv.SetCaptureProperty( - self.capture, cv.CV_CAP_PROP_POS_MSEC, milliseconds) + cv.SetCaptureProperty(self.capture, cv.CV_CAP_PROP_POS_MSEC, + milliseconds) def frame_count(self): """ @@ -120,9 +119,9 @@ class GstVideo(object): size: tuple, optional Size of returned array. sync: bool, optional (default False) - Frames are extracted per frame or per time basis. - If enabled the video time step continues onward according to the play rate. - Useful for ip cameras and other real time video feeds. + Frames are extracted per frame or per time basis. If enabled the video + time step continues onward according to the play rate. Useful for ip + cameras and other real time video feeds. """ def __init__(self, source=None, size=None, sync=False): if not gstreamer_available: @@ -178,7 +177,7 @@ class GstVideo(object): self.appsink.set_property('caps', gst.caps_from_string(caps)) if self.pipeline.set_state(gst.STATE_PLAYING) == gst.STATE_CHANGE_FAILURE: raise NameError("Failed to load video source %s" % self.source) - buff = self.appsink.emit('pull-preroll') + self.appsink.emit('pull-preroll') def get(self): """ @@ -190,7 +189,8 @@ class GstVideo(object): Retrieved image. """ buff = self.appsink.emit('pull-buffer') - img_mat = np.ndarray(shape=(self.size[1], self.size[0], 3), dtype=np.uint8, buffer=buff.data) + img_mat = np.ndarray(shape=(self.size[1], self.size[0], 3), + dtype=np.uint8, buffer=buff.data) return img_mat def seek_frame(self, frame_number): From 1251f77d6aadabb91dff887fd2c57622616b4123 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 22 Jun 2012 23:51:57 +0200 Subject: [PATCH 008/648] COSMIT minor pep8 --- skimage/io/tests/test_colormixer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skimage/io/tests/test_colormixer.py b/skimage/io/tests/test_colormixer.py index f83a34d5..b9f362a2 100644 --- a/skimage/io/tests/test_colormixer.py +++ b/skimage/io/tests/test_colormixer.py @@ -3,6 +3,7 @@ import numpy as np import skimage.io._plugins._colormixer as cm + class ColorMixerTest(object): def setup(self): self.state = np.ones((18, 33, 3), dtype=np.uint8) * 200 @@ -91,7 +92,8 @@ class TestColorMixer(object): def test_gamma(self): gamma = 1.5 cm.gamma(self.img, self.state, gamma) - img = np.asarray(((self.state/255.)**(1/gamma))*255, dtype='uint8') + img = np.asarray(((self.state / 255.)**(1 / gamma)) * 255, + dtype='uint8') assert_array_almost_equal(img, self.img) def test_rgb_2_hsv(self): @@ -112,7 +114,6 @@ class TestColorMixer(object): assert_almost_equal(np.array([g]), np.array([0])) assert_almost_equal(np.array([b]), np.array([0])) - def test_hsv_add(self): cm.hsv_add(self.img, self.state, 360, 0, 0) assert_almost_equal(self.img, self.state) @@ -123,7 +124,7 @@ class TestColorMixer(object): def test_hsv_add_clip_pos(self): cm.hsv_add(self.img, self.state, 0, 0, 1) - assert_equal(self.img, np.ones_like(self.state)*255) + assert_equal(self.img, np.ones_like(self.state) * 255) def test_hsv_mul(self): cm.hsv_multiply(self.img, self.state, 360, 1, 1) From f7c56202d0e195b7a3144523fcb3d4683acdc126 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 22 Jun 2012 23:54:33 +0200 Subject: [PATCH 009/648] COSMIT no spaces around power ``**``. Fun: https://gist.github.com/1671995 --- skimage/feature/greycomatrix.py | 12 ++++++------ skimage/feature/harris.py | 2 +- skimage/feature/hog.py | 4 ++-- skimage/filter/edges.py | 4 ++-- skimage/filter/lpi_filter.py | 2 +- skimage/filter/tests/test_tv_denoise.py | 6 +++--- skimage/filter/thresholding.py | 2 +- skimage/filter/tv_denoise.py | 8 ++++---- skimage/measure/_regionprops.py | 4 ++-- skimage/measure/tests/test_find_contours.py | 2 +- skimage/morphology/selem.py | 4 ++-- skimage/morphology/skeletonize.py | 10 +++++----- skimage/morphology/tests/test_skeletonize.py | 4 ++-- skimage/morphology/tests/test_watershed.py | 2 +- .../segmentation/random_walker_segmentation.py | 2 +- skimage/transform/_warp_zoo.py | 2 +- skimage/transform/radon_transform.py | 6 +++--- skimage/util/dtype.py | 16 ++++++++-------- skimage/util/montage.py | 2 +- 19 files changed, 47 insertions(+), 47 deletions(-) diff --git a/skimage/feature/greycomatrix.py b/skimage/feature/greycomatrix.py index 34e05352..5b2b92db 100644 --- a/skimage/feature/greycomatrix.py +++ b/skimage/feature/greycomatrix.py @@ -181,11 +181,11 @@ def greycoprops(P, prop='contrast'): # create weights for specified property I, J = np.ogrid[0:num_level, 0:num_level] if prop == 'contrast': - weights = (I - J) ** 2 + weights = (I - J)**2 elif prop == 'dissimilarity': weights = np.abs(I - J) elif prop == 'homogeneity': - weights = 1. / (1. + (I - J) ** 2) + weights = 1. / (1. + (I - J)**2) elif prop in ['ASM', 'energy', 'correlation']: pass else: @@ -193,10 +193,10 @@ def greycoprops(P, prop='contrast'): # compute property for each GLCM if prop == 'energy': - asm = np.apply_over_axes(np.sum, (P ** 2), axes=(0, 1))[0, 0] + 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] + 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)) @@ -204,9 +204,9 @@ def greycoprops(P, prop='contrast'): 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), + 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), + 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] diff --git a/skimage/feature/harris.py b/skimage/feature/harris.py index e496b33d..14854ac4 100644 --- a/skimage/feature/harris.py +++ b/skimage/feature/harris.py @@ -42,7 +42,7 @@ def _compute_harris_response(image, eps=1e-6, gaussian_deviation=1): Wyy = ndimage.gaussian_filter(imy * imy, 1.5, mode='constant') # determinant and trace - Wdet = Wxx * Wyy - Wxy ** 2 + Wdet = Wxx * Wyy - Wxy**2 Wtr = Wxx + Wyy # Alternate formula for Harris response. # Alison Noble, "Descriptions of Image Surfaces", PhD thesis (1989) diff --git a/skimage/feature/hog.py b/skimage/feature/hog.py index e0e1301d..4a24b0a2 100644 --- a/skimage/feature/hog.py +++ b/skimage/feature/hog.py @@ -95,7 +95,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), cell are used to vote into the orientation histogram. """ - magnitude = sqrt(gx ** 2 + gy ** 2) + magnitude = sqrt(gx**2 + gy**2) orientation = arctan2(gy, (gx + 1e-15)) * (180 / pi) + 90 sy, sx = image.shape @@ -166,7 +166,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), for y in range(n_blocksy): block = orientation_histogram[y:y + by, x:x + bx, :] eps = 1e-5 - normalised_blocks[y, x, :] = block / sqrt(block.sum() ** 2 + eps) + normalised_blocks[y, x, :] = block / sqrt(block.sum()**2 + eps) """ The final step collects the HOG descriptors from all blocks of a dense diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index 6ceb5c5f..b6a800c9 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -37,7 +37,7 @@ def sobel(image, mask=None): Note that ``scipy.ndimage.sobel`` returns a directional Sobel which has to be further processed to perform edge detection. """ - return np.sqrt(hsobel(image, mask) ** 2 + vsobel(image, mask) ** 2) + return np.sqrt(hsobel(image, mask)**2 + vsobel(image, mask)**2) def hsobel(image, mask=None): @@ -137,7 +137,7 @@ def prewitt(image, mask=None): Return the square root of the sum of squares of the horizontal and vertical Prewitt transforms. """ - return np.sqrt(hprewitt(image, mask) ** 2 + vprewitt(image, mask) ** 2) + return np.sqrt(hprewitt(image, mask)**2 + vprewitt(image, mask)**2) def hprewitt(image, mask=None): diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index 92490abe..60eb1d63 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -232,7 +232,7 @@ def wiener(data, impulse_response=None, filter_params={}, K=0.25, F, G = filt._prepare(data) _min_limit(F) - H_mag_sqr = np.abs(F) ** 2 + H_mag_sqr = np.abs(F)**2 F = 1 / F * H_mag_sqr / (H_mag_sqr + K) return _centre(np.abs(ifftshift(np.dual.ifftn(G * F))), data.shape) diff --git a/skimage/filter/tests/test_tv_denoise.py b/skimage/filter/tests/test_tv_denoise.py index 27db4894..4cc6adbb 100644 --- a/skimage/filter/tests/test_tv_denoise.py +++ b/skimage/filter/tests/test_tv_denoise.py @@ -27,8 +27,8 @@ class TestTvDenoise(): grad_denoised = ndimage.morphological_gradient( denoised_lena, size=((3, 3))) # test if the total variation has decreased - assert (np.sqrt((grad_denoised ** 2).sum()) - < np.sqrt((grad ** 2).sum()) / 2) + assert (np.sqrt((grad_denoised**2).sum()) + < np.sqrt((grad**2).sum()) / 2) denoised_lena_int = filter.tv_denoise(img_as_uint(lena), weight=60.0, keep_type=True) assert denoised_lena_int.dtype is np.dtype('uint16') @@ -39,7 +39,7 @@ class TestTvDenoise(): a sphere. """ x, y, z = np.ogrid[0:40, 0:40, 0:40] - mask = (x - 22) ** 2 + (y - 20) ** 2 + (z - 17) ** 2 < 8 ** 2 + mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 mask = 100 * mask.astype(np.float) mask += 60 mask += 20 * np.random.randn(*mask.shape) diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 505123b7..022cacc5 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -127,7 +127,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] diff --git a/skimage/filter/tv_denoise.py b/skimage/filter/tv_denoise.py index 37abdbb5..c8e3216c 100644 --- a/skimage/filter/tv_denoise.py +++ b/skimage/filter/tv_denoise.py @@ -56,12 +56,12 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): d[:, :, 1:] += pz[:, :, :-1] out = im + d - E = (d ** 2).sum() + E = (d**2).sum() gx[:-1] = np.diff(out, axis=0) gy[:, :-1] = np.diff(out, axis=1) gz[:, :, :-1] = np.diff(out, axis=2) - norm = np.sqrt(gx ** 2 + gy ** 2 + gz ** 2) + norm = np.sqrt(gx**2 + gy**2 + gz**2) E += weight * norm.sum() norm *= 0.5 / weight norm += 1. @@ -147,10 +147,10 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): d[:, 1:] += py[:, :-1] out = im + d - E = (d ** 2).sum() + E = (d**2).sum() gx[:-1] = np.diff(out, axis=0) gy[:, :-1] = np.diff(out, axis=1) - norm = np.sqrt(gx ** 2 + gy ** 2) + norm = np.sqrt(gx**2 + gy**2) E += weight * norm.sum() norm *= 0.5 / weight norm += 1 diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 89dbddce..6b8f6a3c 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -210,8 +210,8 @@ def regionprops(label_image, properties=['Area', 'Centroid'], b = mu[1, 1] / mu[0, 0] c = mu[0, 2] / mu[0, 0] #: eigen values of inertia tensor - l1 = (a + c) / 2 + sqrt(4 * b ** 2 + (a - c) ** 2) / 2 - l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 + l1 = (a + c) / 2 + sqrt(4 * b**2 + (a - c)**2) / 2 + l2 = (a + c) / 2 - sqrt(4 * b**2 + (a - c)**2) / 2 # cached results which are used by several properties _filled_image = None diff --git a/skimage/measure/tests/test_find_contours.py b/skimage/measure/tests/test_find_contours.py index 1c6096ee..3de7acc4 100644 --- a/skimage/measure/tests/test_find_contours.py +++ b/skimage/measure/tests/test_find_contours.py @@ -17,7 +17,7 @@ a[1, 1:-1] = 0 ## [ 1., 1., 1., 1., 1., 1., 1., 1.]], dtype=float32) x, y = np.mgrid[-1:1:5j, -1:1:5j] -r = np.sqrt(x ** 2 + y ** 2) +r = np.sqrt(x**2 + y**2) def test_binary(): diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 5ff6793a..f2234ffb 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -111,6 +111,6 @@ def disk(radius, dtype=np.uint8): """ L = np.linspace(-radius, radius, 2 * radius + 1) (X, Y) = np.meshgrid(L, L) - s = X ** 2 - s += Y ** 2 + s = X**2 + s += Y**2 return np.array(s <= radius * radius, dtype=dtype) diff --git a/skimage/morphology/skeletonize.py b/skimage/morphology/skeletonize.py index b7c59ac1..bfa7525c 100644 --- a/skimage/morphology/skeletonize.py +++ b/skimage/morphology/skeletonize.py @@ -244,11 +244,11 @@ def medial_axis(image, mask=None, return_distance=False): # OR # 3. Keep if # pixels in neighbourhood is 2 or less # Note that table is independent of image - center_is_foreground = (np.arange(512) & 2 ** 4).astype(bool) + center_is_foreground = (np.arange(512) & 2**4).astype(bool) table = (center_is_foreground # condition 1. & (np.array([ndimage.label(_pattern_of(index), _eight_connect)[1] != - ndimage.label(_pattern_of(index & ~ 2 ** 4), + ndimage.label(_pattern_of(index & ~ 2**4), _eight_connect)[1] for index in range(512)]) # condition 2 | @@ -311,9 +311,9 @@ def _pattern_of(index): Return the pattern represented by an index value Byte decomposition of index """ - return np.array([[index & 2 ** 0, index & 2 ** 1, index & 2 ** 2], - [index & 2 ** 3, index & 2 ** 4, index & 2 ** 5], - [index & 2 ** 6, index & 2 ** 7, index & 2 ** 8]], bool) + return np.array([[index & 2**0, index & 2**1, index & 2**2], + [index & 2**3, index & 2**4, index & 2**5], + [index & 2**6, index & 2**7, index & 2**8]], bool) def _table_lookup(image, table): diff --git a/skimage/morphology/tests/test_skeletonize.py b/skimage/morphology/tests/test_skeletonize.py index 408e971c..2f9d046e 100644 --- a/skimage/morphology/tests/test_skeletonize.py +++ b/skimage/morphology/tests/test_skeletonize.py @@ -80,8 +80,8 @@ class TestSkeletonize(): # foreground object 3 ir, ic = np.indices(image.shape) - circle1 = (ic - 135) ** 2 + (ir - 150) ** 2 < 30 ** 2 - circle2 = (ic - 135) ** 2 + (ir - 150) ** 2 < 20 ** 2 + circle1 = (ic - 135)**2 + (ir - 150)**2 < 30**2 + circle2 = (ic - 135)**2 + (ir - 150)**2 < 20**2 image[circle1] = 1 image[circle2] = 0 result = skeletonize(image) diff --git a/skimage/morphology/tests/test_watershed.py b/skimage/morphology/tests/test_watershed.py index 1fe8baba..c6671d7a 100644 --- a/skimage/morphology/tests/test_watershed.py +++ b/skimage/morphology/tests/test_watershed.py @@ -72,7 +72,7 @@ def diff(a, b): a = a.astype(np.float64) b = np.asarray(b) b = b.astype(np.float64) - t = ((a - b) ** 2).sum() + t = ((a - b)**2).sum() return math.sqrt(t) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 130be926..68afcc65 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -63,7 +63,7 @@ def _make_graph_edges_3d(n_x, n_y, n_z): def _compute_weights_3d(data, beta=130, eps=1.e-6): - gradients = _compute_gradients_3d(data) ** 2 + gradients = _compute_gradients_3d(data)**2 beta /= 10 * data.std() gradients *= beta weights = np.exp(- gradients) diff --git a/skimage/transform/_warp_zoo.py b/skimage/transform/_warp_zoo.py index c2e8b4df..4df531d1 100644 --- a/skimage/transform/_warp_zoo.py +++ b/skimage/transform/_warp_zoo.py @@ -7,7 +7,7 @@ from ._warp import warp def _swirl_mapping(xy, center, rotation, strength, radius): x, y = xy.T x0, y0 = center - rho = np.sqrt((x - x0) ** 2 + (y - y0) ** 2) + rho = np.sqrt((x - x0)**2 + (y - y0)**2) # Ensure that the transformation decays to approximately 1/1000-th # within the specified radius. diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 2480c7fc..d185c0fc 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -43,7 +43,7 @@ def radon(image, theta=None): if theta == None: theta = np.arange(180) height, width = image.shape - diagonal = np.sqrt(height ** 2 + width ** 2) + diagonal = np.sqrt(height**2 + width**2) heightpad = np.ceil(diagonal - height) widthpad = np.ceil(diagonal - width) padded_image = np.zeros((int(height + heightpad), @@ -130,13 +130,13 @@ def iradon(radon_image, theta=None, output_size=None, th = (np.pi / 180.0) * theta # if output size not specified, estimate from input radon image if not output_size: - output_size = int(np.floor(np.sqrt((radon_image.shape[0]) ** 2 / 2.0))) + output_size = int(np.floor(np.sqrt((radon_image.shape[0])**2 / 2.0))) n = radon_image.shape[0] img = radon_image.copy() # resize image to next power of two for fourier analysis # speeds up fourier and lessens artifacts - order = max(64., 2 ** np.ceil(np.log(2 * n) / np.log(2))) + order = max(64., 2**np.ceil(np.log(2 * n) / np.log(2))) # zero pad input image img.resize((order, img.shape[1])) # construct the fourier filter diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index 6383c53f..9697e627 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -109,21 +109,21 @@ def convert(image, dtype, force_copy=False, uniform=False): prec_loss() if copy: b = np.empty(a.shape, _dtype2(kind, m)) - np.floor_divide(a, 2 ** (n - m), out=b, dtype=a.dtype, + np.floor_divide(a, 2**(n - m), out=b, dtype=a.dtype, casting='unsafe') return b else: - a //= 2 ** (n - m) + a //= 2**(n - m) return a elif m % n == 0: # exact upscale to a multiple of n bits if copy: b = np.empty(a.shape, _dtype2(kind, m)) - np.multiply(a, (2 ** m - 1) // (2 ** n - 1), out=b, dtype=b.dtype) + np.multiply(a, (2**m - 1) // (2**n - 1), out=b, dtype=b.dtype) return b else: a = np.array(a, _dtype2(kind, m, a.dtype.itemsize), copy=False) - a *= (2 ** m - 1) // (2 ** n - 1) + a *= (2**m - 1) // (2**n - 1) return a else: # upscale to a multiple of n bits, @@ -132,13 +132,13 @@ def convert(image, dtype, force_copy=False, uniform=False): o = (m // n + 1) * n if copy: b = np.empty(a.shape, _dtype2(kind, o)) - np.multiply(a, (2 ** o - 1) // (2 ** n - 1), out=b, dtype=b.dtype) - b //= 2 ** (o - m) + np.multiply(a, (2**o - 1) // (2**n - 1), out=b, dtype=b.dtype) + b //= 2**(o - m) return b else: a = np.array(a, _dtype2(kind, o, a.dtype.itemsize), copy=False) - a *= (2 ** o - 1) // (2 ** n - 1) - a //= 2 ** (o - m) + a *= (2**o - 1) // (2**n - 1) + a //= 2**(o - m) return a kind = dtypeobj.kind diff --git a/skimage/util/montage.py b/skimage/util/montage.py index 57ec7274..6c23ccbf 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -84,7 +84,7 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): if fill == 'mean': fill = arr_in.mean() - n_missing = int((alpha ** 2.) - n_images) + n_missing = int((alpha**2.) - n_images) missing = np.ones((n_missing, height, width), dtype=arr_in.dtype) * fill arr_out = np.vstack((arr_in, missing)) From d7f1a3abec72b4043180377f86242348945078b6 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Wed, 27 Jun 2012 21:00:55 +0200 Subject: [PATCH 010/648] COSMIT minor stype improvements, whitespace in error messages --- skimage/filter/tv_denoise.py | 2 +- skimage/graph/tests/test_mcp.py | 4 +-- skimage/io/_plugins/freeimage_plugin.py | 40 ++++++++++++------------- skimage/io/_plugins/util.py | 2 +- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/skimage/filter/tv_denoise.py b/skimage/filter/tv_denoise.py index c8e3216c..e8736786 100644 --- a/skimage/filter/tv_denoise.py +++ b/skimage/filter/tv_denoise.py @@ -252,7 +252,7 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): elif im.ndim == 3: out = _tv_denoise_3d(im, weight, eps, n_iter_max) else: - raise ValueError('only 2-d and 3-d images may be denoised with this' + raise ValueError('only 2-d and 3-d images may be denoised with this ' 'function') if keep_type: return out.astype(im_type) diff --git a/skimage/graph/tests/test_mcp.py b/skimage/graph/tests/test_mcp.py index e7b75cdc..e3fd45a0 100644 --- a/skimage/graph/tests/test_mcp.py +++ b/skimage/graph/tests/test_mcp.py @@ -72,8 +72,8 @@ def test_neg_inf(): def test_route(): - return_path, cost = mcp.route_through_array( - a, (1, 6), (7, 2), geometric=True) + return_path, cost = mcp.route_through_array(a, (1, 6), (7, 2), + geometric=True) assert_almost_equal(cost, np.sqrt(2) / 2) assert_array_equal(return_path, [(1, 6), diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index a43ff749..bbb1772b 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -73,7 +73,7 @@ def load_freeimage(): # candidate libs err_txt = ['%s:\n%s' % (l, str(e.message)) for l, e in errors] raise OSError('One or more FreeImage libraries were found, but ' - 'could not be loaded due to the following errors:\n' + + 'could not be loaded due to the following errors:\n' '\n\n'.join(err_txt)) else: # No errors, because no potential libraries found at all! @@ -211,7 +211,7 @@ class FI_TYPES(object): class IO_FLAGS(object): FIF_LOAD_NOPIXELS = 0x8000 # loading: load the image header only - # (not supported by all plugins) + # (not supported by all plugins) BMP_DEFAULT = 0 BMP_SAVE_RLE = 1 @@ -230,41 +230,41 @@ class IO_FLAGS(object): FAXG3_DEFAULT = 0 GIF_DEFAULT = 0 GIF_LOAD256 = 1 # Load the image as a 256 color image with ununsed - # palette entries, if it's 16 or 2 color + # palette entries, if it's 16 or 2 color GIF_PLAYBACK = 2 # 'Play' the GIF to generate each frame (as 32bpp) - # instead of returning raw frame data when loading + # instead of returning raw frame data when loading HDR_DEFAULT = 0 ICO_DEFAULT = 0 ICO_MAKEALPHA = 1 # convert to 32bpp and create an alpha channel from the - # AND-mask when loading + # AND-mask when loading IFF_DEFAULT = 0 J2K_DEFAULT = 0 # save with a 16:1 rate JP2_DEFAULT = 0 # save with a 16:1 rate JPEG_DEFAULT = 0 # loading (see JPEG_FAST); - # saving (see JPEG_QUALITYGOOD|JPEG_SUBSAMPLING_420) + # saving (see JPEG_QUALITYGOOD|JPEG_SUBSAMPLING_420) JPEG_FAST = 0x0001 # load the file as fast as possible, - # sacrificing some quality + # sacrificing some quality JPEG_ACCURATE = 0x0002 # load the file with the best quality, - # sacrificing some speed + # sacrificing some speed JPEG_CMYK = 0x0004 # load separated CMYK "as is" - # (use | to combine with other load flags) + # (use | to combine with other load flags) JPEG_EXIFROTATE = 0x0008 # load and rotate according to - # Exif 'Orientation' tag if available + # Exif 'Orientation' tag if available JPEG_QUALITYSUPERB = 0x80 # save with superb quality (100:1) JPEG_QUALITYGOOD = 0x0100 # save with good quality (75:1) JPEG_QUALITYNORMAL = 0x0200 # save with normal quality (50:1) JPEG_QUALITYAVERAGE = 0x0400 # save with average quality (25:1) JPEG_QUALITYBAD = 0x0800 # save with bad quality (10:1) JPEG_PROGRESSIVE = 0x2000 # save as a progressive-JPEG - # (use | to combine with other save flags) + # (use | to combine with other save flags) JPEG_SUBSAMPLING_411 = 0x1000 # save with high 4x1 chroma - # subsampling (4:1:1) + # subsampling (4:1:1) JPEG_SUBSAMPLING_420 = 0x4000 # save with medium 2x2 medium chroma - # subsampling (4:2:0) - default value + # subsampling (4:2:0) - default value JPEG_SUBSAMPLING_422 = 0x8000 # save with low 2x1 chroma subsampling (4:2:2) JPEG_SUBSAMPLING_444 = 0x10000 # save with no chroma subsampling (4:4:4) JPEG_OPTIMIZE = 0x20000 # on saving, compute optimal Huffman coding tables - # (can reduce a few percent of file size) + # (can reduce a few percent of file size) JPEG_BASELINE = 0x40000 # save basic JPEG, without metadata or any markers KOALA_DEFAULT = 0 LBM_DEFAULT = 0 @@ -279,14 +279,14 @@ class IO_FLAGS(object): PNG_DEFAULT = 0 PNG_IGNOREGAMMA = 1 # loading: avoid gamma correction PNG_Z_BEST_SPEED = 0x0001 # save using ZLib level 1 compression flag - # (default value is 6) + # (default value is 6) PNG_Z_DEFAULT_COMPRESSION = 0x0006 # save using ZLib level 6 compression - # flag (default recommended value) + # flag (default recommended value) PNG_Z_BEST_COMPRESSION = 0x0009 # save using ZLib level 9 compression flag - # (default value is 6) + # (default value is 6) PNG_Z_NO_COMPRESSION = 0x0100 # save without ZLib compression PNG_INTERLACED = 0x0200 # save using Adam7 interlacing (use | to combine - # with other save flags) + # with other save flags) PNM_DEFAULT = 0 PNM_SAVE_RAW = 0 # Writer saves in RAW format (i.e. P4, P5 or P6) PNM_SAVE_ASCII = 1 # Writer saves in ASCII format (i.e. P1, P2 or P3) @@ -296,7 +296,7 @@ class IO_FLAGS(object): RAS_DEFAULT = 0 RAW_DEFAULT = 0 # load the file as linear RGB 48-bit RAW_PREVIEW = 1 # try to load the embedded JPEG preview with included - # Exif Data or default to RGB 24-bit + # Exif Data or default to RGB 24-bit RAW_DISPLAY = 2 # load the file as RGB 24-bit SGI_DEFAULT = 0 TARGA_DEFAULT = 0 @@ -304,7 +304,7 @@ class IO_FLAGS(object): TARGA_SAVE_RLE = 2 # Save with RLE compression TIFF_DEFAULT = 0 TIFF_CMYK = 0x0001 # reads/stores tags for separated CMYK - # (use | to combine with compression flags) + # (use | to combine with compression flags) TIFF_PACKBITS = 0x0100 # save using PACKBITS compression TIFF_DEFLATE = 0x0200 # save using DEFLATE (a.k.a. ZLIB) compression TIFF_ADOBE_DEFLATE = 0x0400 # save using ADOBE DEFLATE compression diff --git a/skimage/io/_plugins/util.py b/skimage/io/_plugins/util.py index 7c095e82..ed547222 100644 --- a/skimage/io/_plugins/util.py +++ b/skimage/io/_plugins/util.py @@ -64,7 +64,7 @@ class WindowManager(object): self._gui_lock = False self._guikit = '' else: - raise RuntimeError('Only the toolkit that owns the lock may' + raise RuntimeError('Only the toolkit that owns the lock may ' 'release it') def add_window(self, win): From 46e959a9d9e13c880dd5687cb0024de7e0888d2e Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Wed, 27 Jun 2012 21:27:26 +0200 Subject: [PATCH 011/648] COSMIT some manual pep8, removed unused imports, removed unused variables and fixed a bug in a ValueError statement. --- skimage/__init__.py | 1 - skimage/data/__init__.py | 5 +- skimage/data/tests/test_data.py | 5 +- skimage/filter/edges.py | 3 +- skimage/measure/find_contours.py | 12 +- skimage/measure/tests/test_find_contours.py | 64 ++-- skimage/morphology/skeletonize.py | 15 +- skimage/morphology/tests/test_ccomp.py | 1 + skimage/morphology/tests/test_watershed.py | 338 +++++++++--------- skimage/morphology/watershed.py | 23 +- skimage/transform/radon_transform.py | 1 - .../transform/tests/test_hough_transform.py | 6 +- skimage/transform/tests/test_project.py | 9 +- .../transform/tests/test_radon_transform.py | 5 +- 14 files changed, 241 insertions(+), 247 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index a48ce4b2..a3deb6c6 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -63,7 +63,6 @@ except ImportError: def _setup_test(verbose=False): - import gzip import functools args = ['', '--exe', '-w', pkg_dir] diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index 5a01a493..7e351293 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -49,7 +49,7 @@ def lena(): def text(): - """ Gray-level "text" image used for corner detection. + """ Gray-level "text" image used for corner detection. Notes ----- @@ -60,7 +60,8 @@ def text(): """ - return load("text.png") + return load("text.png") + def checkerboard(): """Checkerboard image. diff --git a/skimage/data/tests/test_data.py b/skimage/data/tests/test_data.py index c4c57f86..1d85802b 100644 --- a/skimage/data/tests/test_data.py +++ b/skimage/data/tests/test_data.py @@ -1,6 +1,5 @@ import skimage.data as data -from numpy.testing import assert_equal, assert_array_equal -import numpy as np +from numpy.testing import assert_equal def test_lena(): @@ -17,7 +16,7 @@ def test_camera(): def test_checkerboard(): """ Test that checkerboard image can be loaded. """ - checkerboard = data.checkerboard() + data.checkerboard() if __name__ == "__main__": from numpy.testing import run_module_suite diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index b6a800c9..cd91effb 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -70,7 +70,7 @@ def hsobel(image, mask=None): mask = np.ones(image.shape, bool) big_mask = binary_erosion(mask, generate_binary_structure(2, 2), - border_value = 0) + border_value=0) result = np.abs(convolve(image, np.array([[ 1, 2, 1], [ 0, 0, 0], @@ -78,6 +78,7 @@ def hsobel(image, mask=None): result[big_mask == False] = 0 return result + def vsobel(image, mask=None): """Find the vertical edges of an image using the Sobel transform. diff --git a/skimage/measure/find_contours.py b/skimage/measure/find_contours.py index a64d63ea..68e1d53c 100755 --- a/skimage/measure/find_contours.py +++ b/skimage/measure/find_contours.py @@ -5,6 +5,7 @@ from collections import deque _param_options = ('high', 'low') + def find_contours(array, level, fully_connected='low', positive_orientation='low'): """Find iso-valued contours in a 2D array for a given level value. @@ -83,10 +84,10 @@ def find_contours(array, level, This means that to find reasonable contours, it is best to find contours midway between the expected "light" and "dark" values. In particular, - given a binarized array, *do not* choose to find contours at the low or high - value of the array. This will often yield degenerate contours, especially - around structures that are a single array element wide. Instead choose - a middle value, as above. + given a binarized array, *do not* choose to find contours at the low or + high value of the array. This will often yield degenerate contours, + especially around structures that are a single array element wide. Instead + choose a middle value, as above. References ---------- @@ -129,7 +130,8 @@ def _assemble_contours(points_iterator): # This happens when (and only when) one vertex of the square is # exactly the contour level, and the rest are above or below. # This degnerate vertex will be picked up later by neighboring squares. - if from_point == to_point: continue + if from_point == to_point: + continue tail_data = starts.get(to_point) head_data = ends.get(from_point) diff --git a/skimage/measure/tests/test_find_contours.py b/skimage/measure/tests/test_find_contours.py index 3de7acc4..fbc502f7 100644 --- a/skimage/measure/tests/test_find_contours.py +++ b/skimage/measure/tests/test_find_contours.py @@ -21,39 +21,40 @@ r = np.sqrt(x**2 + y**2) def test_binary(): - contours = find_contours(a, 0.5) - assert len(contours) == 1 - assert_array_equal(contours[0], - [[ 6. , 1.5], - [ 5. , 1.5], - [ 4. , 1.5], - [ 3. , 1.5], - [ 2. , 1.5], - [ 1.5, 2. ], - [ 1.5, 3. ], - [ 1.5, 4. ], - [ 1.5, 5. ], - [ 1.5, 6. ], - [ 1. , 6.5], - [ 0.5, 6. ], - [ 0.5, 5. ], - [ 0.5, 4. ], - [ 0.5, 3. ], - [ 0.5, 2. ], - [ 0.5, 1. ], - [ 1. , 0.5], - [ 2. , 0.5], - [ 3. , 0.5], - [ 4. , 0.5], - [ 5. , 0.5], - [ 6. , 0.5], - [ 6.5, 1. ], - [ 6. , 1.5]]) + contours = find_contours(a, 0.5) + assert len(contours) == 1 + assert_array_equal(contours[0], + [[6. , 1.5], + [5. , 1.5], + [4. , 1.5], + [3. , 1.5], + [2. , 1.5], + [1.5, 2. ], + [1.5, 3. ], + [1.5, 4. ], + [1.5, 5. ], + [1.5, 6. ], + [1. , 6.5], + [0.5, 6. ], + [0.5, 5. ], + [0.5, 4. ], + [0.5, 3. ], + [0.5, 2. ], + [0.5, 1. ], + [1. , 0.5], + [2. , 0.5], + [3. , 0.5], + [4. , 0.5], + [5. , 0.5], + [6. , 0.5], + [6.5, 1. ], + [6. , 1.5]]) + def test_float(): - contours = find_contours(r, 0.5) - assert len(contours) == 1 - assert_array_equal(contours[0], + contours = find_contours(r, 0.5) + assert len(contours) == 1 + assert_array_equal(contours[0], [[ 2., 3.], [ 1., 2.], [ 2., 1.], @@ -61,7 +62,6 @@ def test_float(): [ 2., 3.]]) - if __name__ == '__main__': from numpy.testing import run_module_suite run_module_suite() diff --git a/skimage/morphology/skeletonize.py b/skimage/morphology/skeletonize.py index bfa7525c..a8d31bdd 100644 --- a/skimage/morphology/skeletonize.py +++ b/skimage/morphology/skeletonize.py @@ -79,7 +79,7 @@ def skeletonize(image): [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]], dtype=uint8) - + """ # look up table - there is one entry for each of the 2^8=256 possible # combinations of 8 binary neighbours. 1's, 2's and 3's are candidates @@ -318,9 +318,9 @@ def _pattern_of(index): def _table_lookup(image, table): """ - Perform a morphological transform on an image, directed by its + Perform a morphological transform on an image, directed by its neighbors - + Parameters ---------- image : ndarray @@ -330,7 +330,7 @@ def _table_lookup(image, table): the values of that pixel and its 8-connected neighbors. border_value : bool The value of pixels beyond the border of the image. - + Returns ------- result : ndarray of same shape as `image` @@ -340,7 +340,7 @@ def _table_lookup(image, table): ----- The pixels are numbered like this:: - + 0 1 2 3 4 5 6 7 8 @@ -358,11 +358,11 @@ def _table_lookup(image, table): indexer[1:, 1:] += image[:-1, :-1] * 2**0 indexer[1:, :] += image[:-1, :] * 2**1 indexer[1:, :-1] += image[:-1, 1:] * 2**2 - + indexer[:, 1:] += image[:, :-1] * 2**3 indexer[:, :] += image[:, :] * 2**4 indexer[:, :-1] += image[:, 1:] * 2**5 - + indexer[:-1, 1:] += image[1:, :-1] * 2**6 indexer[:-1, :] += image[1:, :] * 2**7 indexer[:-1, :-1] += image[1:, 1:] * 2**8 @@ -370,4 +370,3 @@ def _table_lookup(image, table): indexer = _table_lookup_index(np.ascontiguousarray(image, np.uint8)) image = table[indexer] return image - diff --git a/skimage/morphology/tests/test_ccomp.py b/skimage/morphology/tests/test_ccomp.py index cb092b4c..1e0829ca 100644 --- a/skimage/morphology/tests/test_ccomp.py +++ b/skimage/morphology/tests/test_ccomp.py @@ -3,6 +3,7 @@ from numpy.testing import assert_array_equal, run_module_suite from skimage.morphology import label + class TestConnectedComponents: def setup(self): self.x = np.array([[0, 0, 3, 2, 1, 9], diff --git a/skimage/morphology/tests/test_watershed.py b/skimage/morphology/tests/test_watershed.py index c6671d7a..46298ae5 100644 --- a/skimage/morphology/tests/test_watershed.py +++ b/skimage/morphology/tests/test_watershed.py @@ -43,7 +43,6 @@ Original author: Lee Kamentsky import math -import time import unittest import numpy as np @@ -54,6 +53,7 @@ from skimage.morphology.watershed import watershed, \ eps = 1e-12 + def diff(a, b): if not isinstance(a, np.ndarray): a = np.asarray(a) @@ -122,28 +122,27 @@ class TestWatershed(unittest.TestCase): def test_watershed02(self): "watershed 2" data = np.array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], np.uint8) + markers = np.array([[-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, 1, 1, 1, 1, 1, 0], - [0, 1, 0, 0, 0, 1, 0], - [0, 1, 0, 0, 0, 1, 0], - [0, 1, 0, 0, 0, 1, 0], - [0, 1, 1, 1, 1, 1, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0]], np.uint8) - markers = np.array([[ -1, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 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]], - np.int8) + [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], + [0, 0, 0, 0, 0, 0, 0]], np.int8) out = watershed(data, markers) error = diff([[-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], @@ -161,26 +160,25 @@ class TestWatershed(unittest.TestCase): def test_watershed03(self): "watershed 3" data = np.array([[0, 0, 0, 0, 0, 0, 0], - [0, 1, 1, 1, 1, 1, 0], - [0, 1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1, 0], - [0, 1, 1, 1, 1, 1, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0]], np.uint8) - markers = 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, 2, 0, 3, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, -1]], - np.int8) + [0, 1, 1, 1, 1, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], np.uint8) + markers = 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, 2, 0, 3, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, -1]], np.int8) out = watershed(data, markers) error = diff([[-1, -1, -1, -1, -1, -1, -1], [-1, 0, 2, 0, 3, 0, -1], @@ -202,21 +200,20 @@ class TestWatershed(unittest.TestCase): [0, 1, 0, 1, 0, 1, 0], [0, 1, 0, 1, 0, 1, 0], [0, 1, 1, 1, 1, 1, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], np.uint8) - markers = 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, 2, 0, 3, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, -1]], - np.int8) + markers = 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, 2, 0, 3, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, -1]], np.int8) out = watershed(data, markers, self.eight) error = diff([[-1, -1, -1, -1, -1, -1, -1], [-1, 2, 2, 0, 3, 3, -1], @@ -224,35 +221,34 @@ class TestWatershed(unittest.TestCase): [-1, 2, 2, 0, 3, 3, -1], [-1, 2, 2, 0, 3, 3, -1], [-1, 2, 2, 0, 3, 3, -1], - [-1, -1, -1, -1, -1, -1, -1], - [-1, -1, -1, -1, -1, -1, -1], - [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]], out) self.failUnless(error < eps) def test_watershed05(self): "watershed 5" data = np.array([[0, 0, 0, 0, 0, 0, 0], - [0, 1, 1, 1, 1, 1, 0], - [0, 1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1, 0], - [0, 1, 0, 1, 0, 1, 0], - [0, 1, 1, 1, 1, 1, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0]], np.uint8) - markers = 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, 3, 0, 2, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 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]], - np.int8) + [0, 1, 1, 1, 1, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], np.uint8) + markers = 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, 3, 0, 2, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 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]], np.int8) out = watershed(data, markers, self.eight) error = diff([[-1, -1, -1, -1, -1, -1, -1], [-1, 3, 3, 0, 2, 2, -1], @@ -269,24 +265,23 @@ class TestWatershed(unittest.TestCase): def test_watershed06(self): "watershed 6" data = np.array([[0, 1, 0, 0, 0, 1, 0], - [0, 1, 0, 0, 0, 1, 0], - [0, 1, 0, 0, 0, 1, 0], - [0, 1, 1, 1, 1, 1, 0], - [0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0]], np.uint8) - markers = np.array([[ 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], - [ 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]], - np.int8) + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], np.uint8) + markers = np.array([[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], + [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]], np.int8) out = watershed(data, markers, self.eight) error = diff([[-1, 1, 1, 1, 1, 1, -1], [-1, 1, 1, 1, 1, 1, -1], @@ -302,28 +297,28 @@ class TestWatershed(unittest.TestCase): def test_watershed07(self): "A regression test of a competitive case that failed" data = np.array([[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], - [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], - [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], - [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], - [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], - [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], - [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], - [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], - [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], - [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], - [255,255,255,255,255,204,153,103,103,153,204,255,255,255,255,255], - [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], - [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], - [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], - [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], - [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], - [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], - [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], - [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], - [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], - [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]]) + [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], + [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], + [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], + [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], + [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], + [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], + [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], + [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], + [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], + [255,255,255,255,255,204,153,103,103,153,204,255,255,255,255,255], + [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], + [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], + [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], + [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], + [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], + [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], + [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], + [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], + [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], + [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]]) mask = (data != 255) - markers = np.zeros(data.shape,int) + markers = np.zeros(data.shape, int) markers[6, 7] = 1 markers[14, 7] = 2 out = watershed(data, markers, self.eight, mask=mask) @@ -338,30 +333,30 @@ class TestWatershed(unittest.TestCase): def test_watershed08(self): "The border pixels + an edge are all the same value" data = np.array([[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], - [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], - [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], - [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], - [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], - [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], - [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], - [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], - [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], - [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], - [255,255,255,255,255,204,153,141,141,153,204,255,255,255,255,255], - [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], - [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], - [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], - [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], - [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], - [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], - [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], - [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], - [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], - [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]]) + [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], + [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], + [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], + [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], + [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], + [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], + [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], + [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], + [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], + [255,255,255,255,255,204,153,141,141,153,204,255,255,255,255,255], + [255,255,255,255,204,183,141, 94, 94,141,183,204,255,255,255,255], + [255,255,255,204,183,141,111, 72, 72,111,141,183,204,255,255,255], + [255,255,204,183,141,111, 72, 39, 39, 72,111,141,183,204,255,255], + [255,255,204,153,111, 72, 39, 1, 1, 39, 72,111,153,204,255,255], + [255,255,204,153,111, 94, 72, 52, 52, 72, 94,111,153,204,255,255], + [255,255,204,183,153,141,111,103,103,111,141,153,183,204,255,255], + [255,255,255,204,204,183,153,153,153,153,183,204,204,255,255,255], + [255,255,255,255,255,204,204,204,204,204,204,255,255,255,255,255], + [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255], + [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]]) mask = (data != 255) - markers = np.zeros(data.shape,int) - markers[6,7] = 1 - markers[14,7] = 2 + markers = np.zeros(data.shape, int) + markers[6, 7] = 1 + markers[14, 7] = 2 out = watershed(data, markers, self.eight, mask=mask) # # The two objects should be the same size, except possibly for the @@ -379,20 +374,17 @@ class TestWatershed(unittest.TestCase): """ image = np.zeros((1000, 1000)) coords = np.random.uniform(0, 1000, (100, 2)).astype(int) - markers = np.zeros((1000, 1000),int) + markers = np.zeros((1000, 1000), int) idx = 1 - for x,y in coords: - image[x,y] = 1 + for x, y in coords: + image[x, y] = 1 markers[x, y] = idx idx += 1 image = scipy.ndimage.gaussian_filter(image, 4) - before = time.clock() - out = watershed(image, markers, self.eight) - elapsed = time.clock() - before - before = time.clock() - out = scipy.ndimage.watershed_ift(image.astype(np.uint16), markers, self.eight) - elapsed = time.clock() - before + watershed(image, markers, self.eight) + scipy.ndimage.watershed_ift(image.astype(np.uint16), markers, + self.eight) class TestIsLocalMaximum(unittest.TestCase): @@ -431,48 +423,48 @@ class TestIsLocalMaximum(unittest.TestCase): self.assertTrue(np.all(result == expected)) def test_01_04_not_adjacent_and_different(self): - image = np.zeros((10,20)) - labels = np.zeros((10,20), int) - image[5,5] = 1 - image[5,8] = .5 + image = np.zeros((10, 20)) + labels = np.zeros((10, 20), int) + image[5, 5] = 1 + image[5, 8] = .5 labels[image > 0] = 1 expected = (labels == 1) - result = is_local_maximum(image, labels, np.ones((3,3), bool)) + result = is_local_maximum(image, labels, np.ones((3, 3), bool)) self.assertTrue(np.all(result == expected)) def test_01_05_two_objects(self): - image = np.zeros((10,20)) - labels = np.zeros((10,20), int) - image[5,5] = 1 - image[5,15] = .5 - labels[5,5] = 1 - labels[5,15] = 2 + image = np.zeros((10, 20)) + labels = np.zeros((10, 20), int) + image[5, 5] = 1 + image[5, 15] = .5 + labels[5, 5] = 1 + labels[5, 15] = 2 expected = (labels > 0) - result = is_local_maximum(image, labels, np.ones((3,3), bool)) + result = is_local_maximum(image, labels, np.ones((3, 3), bool)) self.assertTrue(np.all(result == expected)) def test_01_06_adjacent_different_objects(self): - image = np.zeros((10,20)) - labels = np.zeros((10,20), int) - image[5,5] = 1 - image[5,6] = .5 - labels[5,5] = 1 - labels[5,6] = 2 + image = np.zeros((10, 20)) + labels = np.zeros((10, 20), int) + image[5, 5] = 1 + image[5, 6] = .5 + labels[5, 5] = 1 + labels[5, 6] = 2 expected = (labels > 0) - result = is_local_maximum(image, labels, np.ones((3,3), bool)) + result = is_local_maximum(image, labels, np.ones((3, 3), bool)) self.assertTrue(np.all(result == expected)) def test_02_01_four_quadrants(self): np.random.seed(21) - image = np.random.uniform(size=(40,60)) - i,j = np.mgrid[0:40,0:60] + image = np.random.uniform(size=(40, 60)) + i, j = np.mgrid[0:40, 0:60] labels = 1 + (i >= 20) + (j >= 30) * 2 - i,j = np.mgrid[-3:4,-3:4] + i, j = np.mgrid[-3:4, -3:4] footprint = (i * i + j * j <= 9) expected = np.zeros(image.shape, float) for imin, imax in ((0, 20), (20, 40)): for jmin, jmax in ((0, 30), (30, 60)): - expected[imin:imax,jmin:jmax] = scipy.ndimage.maximum_filter( + expected[imin:imax, jmin:jmax] = scipy.ndimage.maximum_filter( image[imin:imax, jmin:jmax], footprint=footprint) expected = (expected == image) result = is_local_maximum(image, labels, footprint) @@ -484,9 +476,9 @@ class TestIsLocalMaximum(unittest.TestCase): Test is_local_maximum when every point is a local maximum ''' np.random.seed(31) - image = np.random.uniform(size=(10,20)) + image = np.random.uniform(size=(10, 20)) footprint = np.array([[1]]) - result = is_local_maximum(image, np.ones((10,20)), footprint) + result = is_local_maximum(image, np.ones((10, 20)), footprint) self.assertTrue(np.all(result)) result = is_local_maximum(image, footprint=footprint) self.assertTrue(np.all(result)) diff --git a/skimage/morphology/watershed.py b/skimage/morphology/watershed.py index 71eaf113..c0b1b34b 100644 --- a/skimage/morphology/watershed.py +++ b/skimage/morphology/watershed.py @@ -24,13 +24,12 @@ All rights reserved. Original author: Lee Kamentsky """ -from _heapq import heapify, heappush, heappop +from _heapq import heappush, heappop import numpy as np import scipy.ndimage from ..filter import rank_order from . import _watershed -import warnings def watershed(image, markers, connectivity=None, offset=None, mask=None): @@ -123,17 +122,17 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): The algorithm works also for 3-D images, and can be used for example to separate overlapping spheres. """ - + if connectivity == None: c_connectivity = scipy.ndimage.generate_binary_structure(image.ndim, 1) else: c_connectivity = np.array(connectivity, bool) if c_connectivity.ndim != image.ndim: - raise ValueError,"Connectivity dimension must be same as image" + raise ValueError("Connectivity dimension must be same as image") if offset == None: - if any([x%2==0 for x in c_connectivity.shape]): - raise ValueError,"Connectivity array must have an unambiguous \ - center" + if any([x % 2 == 0 for x in c_connectivity.shape]): + raise ValueError("Connectivity array must have an unambiguous " + "center") # # offset to center of connectivity array # @@ -144,7 +143,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): pads = offset def pad(im): - new_im = np.zeros([i + 2*p for i, p in zip(im.shape, pads)], im.dtype) + new_im = np.zeros([i + 2 * p for i, p in zip(im.shape, pads)], im.dtype) new_im[[slice(p, -p, None) for p in pads]] = im return new_im @@ -158,9 +157,8 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): c_image = rank_order(image)[0].astype(np.int32) c_markers = np.ascontiguousarray(markers, dtype=np.int32) if c_markers.ndim != c_image.ndim: - raise ValueError,\ - "markers (ndim=%d) must have same # of dimensions "\ - "as image (ndim=%d)"%(c_markers.ndim, cimage.ndim) + raise ValueError("markers (ndim=%d) must have same # of dimensions " + "as image (ndim=%d)" % (c_markers.ndim, c_image.ndim)) if c_markers.shape != c_image.shape: raise ValueError("image and markers must have the same shape") if mask != None: @@ -190,7 +188,6 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): indexes = [] ignore = True for j in range(len(c_connectivity.shape)): - elems = c_image.shape[j] idx = (i // multiplier) % c_connectivity.shape[j] off = idx - offset[j] if off: @@ -231,7 +228,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): def is_local_maximum(image, labels=None, footprint=None): """ Return a boolean array of points that are local maxima - + Parameters ---------- image: ndarray (2-D, 3-D, ...) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index d185c0fc..b94414c2 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -140,7 +140,6 @@ def iradon(radon_image, theta=None, output_size=None, # zero pad input image img.resize((order, img.shape[1])) # construct the fourier filter - freqs = np.zeros((order, 1)) f = fftshift(abs(np.mgrid[-1:1:2 / order])).reshape(-1, 1) w = 2 * np.pi * f diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 7472ebe0..03b63d07 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -59,8 +59,9 @@ def test_probabilistic_hough(): img[i, i] = 100 # decrease default theta sampling because similar orientations may confuse # as mentioned in article of Galambos et al - theta=np.linspace(0, np.pi, 45) - lines = probabilistic_hough(img, theta=theta, threshold=10, line_length=10, line_gap=1) + theta = np.linspace(0, np.pi, 45) + lines = probabilistic_hough(img, theta=theta, threshold=10, line_length=10, + line_gap=1) # sort the lines according to the x-axis sorted_lines = [] for line in lines: @@ -73,4 +74,3 @@ def test_probabilistic_hough(): if __name__ == "__main__": run_module_suite() - diff --git a/skimage/transform/tests/test_project.py b/skimage/transform/tests/test_project.py index 011c6e9c..25ebda20 100644 --- a/skimage/transform/tests/test_project.py +++ b/skimage/transform/tests/test_project.py @@ -6,6 +6,7 @@ from skimage.transform import homography, fast_homography from skimage import data from skimage.color import rgb2gray + def test_stackcopy(): layers = 4 x = np.empty((3, 3, layers)) @@ -17,10 +18,10 @@ def test_stackcopy(): def test_homography(): x = np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1 - theta = -np.pi/2 - M = np.array([[np.cos(theta),-np.sin(theta),0], - [np.sin(theta), np.cos(theta),2], - [0, 0, 1]]) + theta = -np.pi / 2 + M = np.array([[np.cos(theta), -np.sin(theta), 0], + [np.sin(theta), np.cos(theta), 2], + [0, 0, 1]]) x90 = homography(x, M, order=1) assert_array_almost_equal(x90, np.rot90(x)) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 0c783651..f8e9416f 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -40,6 +40,7 @@ def test_radon_iradon(): image = np.tri(size) + np.tri(size)[::-1] reconstructed = iradon(radon(image), filter="ramp", interpolation="nearest") + def test_iradon_angles(): """ Test with different number of projections @@ -62,10 +63,12 @@ def test_iradon_angles(): s = radon_image_80.sum(axis=0) assert np.allclose(s, s[0], rtol=0.01) reconstructed = iradon(radon_image_80) - delta_80 = np.mean(abs(image/np.max(image) - reconstructed/np.max(reconstructed))) + delta_80 = np.mean(abs(image / np.max(image) - + reconstructed / np.max(reconstructed))) # Loss of quality when the number of projections is reduced assert delta_80 > delta_200 + def test_radon_minimal(): """ Test for small images for various angles From 4d1809a63c3bbdd6d29f240092cde3c4f287bfc8 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 8 Jul 2012 17:49:17 -0700 Subject: [PATCH 012/648] BUG: Allow rgb2grey to be called on grey-level images. --- skimage/color/colorconv.py | 3 +++ skimage/color/tests/test_colorconv.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 274581b5..5e2fa5e1 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -511,6 +511,9 @@ def rgb2grey(rgb): >>> lena = imread(os.path.join(data_dir, 'lena.png')) >>> lena_grey = rgb2grey(lena) """ + if rgb.ndim == 2: + return rgb + return _convert(grey_from_rgb, rgb[:, :, :3])[..., 0] rgb2gray = rgb2grey diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 84c921b8..cfe81c58 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -146,6 +146,9 @@ class TestColorconv(TestCase): assert_equal(g.shape, (1, 1)) + def test_rgb2grey_on_grey(self): + rgb2grey(np.random.random((5, 5))) + def test_gray2rgb(): x = np.array([0, 0.5, 1]) From 1dcc172f60eef548251f45fac1ad26197bcbc78c Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 9 Jul 2012 11:29:37 -0700 Subject: [PATCH 013/648] TST: Check number of input angles in radon tf. --- skimage/transform/radon_transform.py | 17 +++++++++++++++-- skimage/transform/tests/test_radon_transform.py | 7 +++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index b94414c2..b716a6f1 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -40,8 +40,9 @@ def radon(image, theta=None): """ if image.ndim != 2: raise ValueError('The input image must be 2-D') - if theta == None: + if theta is None: theta = np.arange(180) + height, width = image.shape diagonal = np.sqrt(height**2 + width**2) heightpad = np.ceil(diagonal - height) @@ -124,9 +125,17 @@ def iradon(radon_image, theta=None, output_size=None, """ if radon_image.ndim != 2: raise ValueError('The input image must be 2-D') - if theta == None: + + if theta is None: m, n = radon_image.shape theta = np.linspace(0, 180, n, endpoint=False) + else: + theta = np.asarray(theta) + + if len(theta) != radon_image.shape[1]: + raise ValueError("The given ``theta`` does not match the number of " + "projections in ``radon_image``.") + th = (np.pi / 180.0) * theta # if output size not specified, estimate from input radon image if not output_size: @@ -160,9 +169,11 @@ def iradon(radon_image, theta=None, output_size=None, raise ValueError("Unknown filter: %s" % filter) filter_ft = np.tile(f, (1, len(theta))) + # apply filter in fourier domain projection = fft(img, axis=0) * filter_ft radon_filtered = np.real(ifft(projection, axis=0)) + # resize filtered image back to original size radon_filtered = radon_filtered[:radon_image.shape[0], :] reconstructed = np.zeros((output_size, output_size)) @@ -180,6 +191,7 @@ def iradon(radon_image, theta=None, output_size=None, k = np.round(mid_index + xpr * np.sin(th[i]) - ypr * np.cos(th[i])) reconstructed += radon_filtered[ ((((k > 0) & (k < n)) * k) - 1).astype(np.int), i] + elif interpolation == "linear": for i in range(len(theta)): t = xpr * np.sin(th[i]) - ypr * np.cos(th[i]) @@ -189,6 +201,7 @@ def iradon(radon_image, theta=None, output_size=None, b1 = ((((b > 0) & (b < n)) * b) - 1).astype(np.int) reconstructed += (t - a) * radon_filtered[b0, i] + \ (a - t + 1) * radon_filtered[b1, i] + else: raise ValueError("Unknown interpolation: %s" % interpolation) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index f8e9416f..3b2f19dc 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -97,5 +97,12 @@ def test_radon_minimal(): assert np.all(abs(c - reconstructed) < 0.4) +def test_reconstruct_with_wrong_angles(): + a = np.zeros((3, 3)) + p = radon(a, theta=[0, 1, 2]) + iradon(p, theta=[0, 1, 2]) + assert_raises(ValueError, iradon, p, theta=[0, 1, 2, 3]) + + if __name__ == "__main__": run_module_suite() From 8d313f1d5770a62494af58821fa9d95a472c2712 Mon Sep 17 00:00:00 2001 From: cgohlke Date: Thu, 12 Jul 2012 10:34:15 -0700 Subject: [PATCH 014/648] Fix io.imsave() problems with passing arguments to plugin "freeimage" --- skimage/io/_plugins/freeimage_plugin.py | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index bbb1772b..6475781b 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -156,20 +156,20 @@ class FI_TYPES(object): } fi_types = { - (numpy.uint8, 1): FIT_BITMAP, - (numpy.uint8, 3): FIT_BITMAP, - (numpy.uint8, 4): FIT_BITMAP, - (numpy.uint16, 1): FIT_UINT16, - (numpy.int16, 1): FIT_INT16, - (numpy.uint32, 1): FIT_UINT32, - (numpy.int32, 1): FIT_INT32, - (numpy.float32, 1): FIT_FLOAT, - (numpy.float64, 1): FIT_DOUBLE, - (numpy.complex128, 1): FIT_COMPLEX, - (numpy.uint16, 3): FIT_RGB16, - (numpy.uint16, 4): FIT_RGBA16, - (numpy.float32, 3): FIT_RGBF, - (numpy.float32, 4): FIT_RGBAF + (numpy.dtype('uint8'), 1): FIT_BITMAP, + (numpy.dtype('uint8'), 3): FIT_BITMAP, + (numpy.dtype('uint8'), 4): FIT_BITMAP, + (numpy.dtype('uint16'), 1): FIT_UINT16, + (numpy.dtype('int16'), 1): FIT_INT16, + (numpy.dtype('uint32'), 1): FIT_UINT32, + (numpy.dtype('int32'), 1): FIT_INT32, + (numpy.dtype('float32'), 1): FIT_FLOAT, + (numpy.dtype('float64'), 1): FIT_DOUBLE, + (numpy.dtype('complex128'), 1): FIT_COMPLEX, + (numpy.dtype('uint16'), 3): FIT_RGB16, + (numpy.dtype('uint16'), 4): FIT_RGBA16, + (numpy.dtype('float32'), 3): FIT_RGBF, + (numpy.dtype('float32'), 4): FIT_RGBAF } extra_dims = { @@ -624,7 +624,7 @@ def _array_to_bitmap(array): else: n_channels = shape[0] try: - fi_type = FI_TYPES.fi_types[(dtype.type, n_channels)] + fi_type = FI_TYPES.fi_types[(dtype, n_channels)] except KeyError: raise ValueError('Cannot write arrays of given type and shape.') From 25efae8269e5647b17dc69329f412f914d26751c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 13 Jul 2012 00:53:51 -0400 Subject: [PATCH 015/648] Add morphological reconstruction with test and example. --- .../plot_peak_detection_comparison.py | 218 ++++++++++++++++++ skimage/data/noisy_circles.jpg | Bin 0 -> 26206 bytes skimage/morphology/__init__.py | 1 + skimage/morphology/_greyreconstruct.pyx | 85 +++++++ skimage/morphology/greyreconstruct.py | 150 ++++++++++++ skimage/morphology/setup.py | 3 + .../morphology/tests/test_reconstruction.py | 73 ++++++ 7 files changed, 530 insertions(+) create mode 100644 doc/examples/applications/plot_peak_detection_comparison.py create mode 100644 skimage/data/noisy_circles.jpg create mode 100644 skimage/morphology/_greyreconstruct.pyx create mode 100644 skimage/morphology/greyreconstruct.py create mode 100644 skimage/morphology/tests/test_reconstruction.py diff --git a/doc/examples/applications/plot_peak_detection_comparison.py b/doc/examples/applications/plot_peak_detection_comparison.py new file mode 100644 index 00000000..2cf7f933 --- /dev/null +++ b/doc/examples/applications/plot_peak_detection_comparison.py @@ -0,0 +1,218 @@ +""" +============== +Peak detection +============== + +Peak detection (a.k.a. spot detection or particle detection) is a common +image analysis step. For example, it's used to detect tracer particles in a +flow for particle image velocimetry (i.e. PIV) and to identify features in +the Hough transform. + +To simplify plotting code, let's define a simple function that creates a new +figure on each call, and removes tick labels. +""" + +import matplotlib.pyplot as plt + +plt.rcParams['axes.titlesize'] = 10 +plt.rcParams['font.size'] = 10 +def imshow(image, **kwargs): + plt.figure(figsize=(2.5, 2.5)) + plt.imshow(image, **kwargs) + plt.axis('off') + +""" +To explore different peak detection techniques, we use an image of circles with +added noise: +""" + +from skimage import data +img = data.load('noisy_circles.jpg') +imshow(img) + +""" + +.. image:: PLOT2RST.current_figure + +This image is noisy and has uneven background illumination. The peaks in the +image, while readily identified by eye, can be tricky to find algorithmically. +The first thing we need to do is remove the high-frequency noise; this can +be done with a simple Gaussian filter. +""" + +import scipy.ndimage as ndimg +img_smooth = ndimg.gaussian_filter(img, 3) + +imshow(img_smooth) + +""" + +.. image:: PLOT2RST.current_figure + +Thresholding +============ + +One way to extract the background is to threshold the image. +""" + +thresh_value = 100 +background = img_smooth.copy() +background[img_smooth > thresh_value] = 0 +peaks = img_smooth - background + +""" +Here, all pixels values below the threshold value are subtracted from the +image. The resulting background image and the extracted peaks are shown below. +""" + +imshow(background, vmin=0, vmax=255) +plt.title("background image (thresholding)") + +""" +.. image:: PLOT2RST.current_figure +""" + +imshow(peaks, vmin=0, vmax=255) +plt.title("peaks (thresholding)") + +""" +.. image:: PLOT2RST.current_figure + +Because of uneven illumination, peaks on the right bleed into each other. +Increasing the threshold will fix this problem, but it will also cause some +peaks on the left to go undetected. + +Morphological reconstruction +============================ +""" + +import numpy as np +img_r = np.int32(img_smooth) + +import skimage.morphology as morph +h = 20 +rec = morph.reconstruction(img_r-h, img_r) + +imshow(img_r, vmin=0, vmax=255) +plt.title("original (smoothed) image") + +""" +.. image:: PLOT2RST.current_figure +""" + +imshow(rec, vmin=0, vmax=255) +plt.title("background image (reconstruction)") + +""" +.. image:: PLOT2RST.current_figure + +This reconstructed image looks pretty much like the original, except that the +peaks in the image are truncated. The reconstructed image can then be +subtracted from the original image to reveal the peaks of the image. +""" + +imshow(img_r-rec) +plt.title("h-dome of image") + +""" +.. image:: PLOT2RST.current_figure + +The result is known as the h-dome transformation [2]_, which extracts peaks of +height `h` from the original image. To better understand what's going on, +let's take a 1D slice along the middle of the image (cutting through peaks in +the image). +""" + +img_slice = img_r[99:100, :] +rec_slice = morph.reconstruction(img_slice-h, img_slice) + +""" +Plotting the reconstructed image (slice) next to the original image and the +seed image shed light on the reconstruction process +""" +plt.figure(figsize=(4, 3)) +plt.plot(img_slice[0], 'k', label='original image') +plt.plot(img_slice[0]-h, '0.5', label='seed image') +plt.plot(rec_slice[0], 'r', label='reconstructed') +plt.title("image slice") +plt.xlabel('x') +plt.ylabel('intensity') +plt.legend() + +""" +.. image:: PLOT2RST.current_figure + +Here, you see that morphological reconstruction dilates the seed image (i.e. +the `h`-shifted image) until it intersects the mask (original image). Note that +the peaks in the original image have very different intensity values (e.g. the +peak at x=200 and x=100 differ by about 80). Subtracting the reconstructed +image from the original image gives peaks of roughly equal intensity. Thus, the +h-dome transformation is quiet effective at removing uneven, dark backgrounds +from bright features. The inverse operation---the h-basin +transformation---should be used when removing bright backgrounds from dark +features. + + +White tophat +============ +""" +selem = morph.disk(10) +img_t = np.uint8(img_smooth) +opening = morph.greyscale_open(img_t, selem) +top_hat = img_t - opening + +imshow(opening, vmin=0, vmax=255) +plt.title("Greyscale opening of image") + +""" +.. image:: PLOT2RST.current_figure +""" + + +imshow(top_hat) +plt.title("Tophat with disk of r = 10") + +""" +.. image:: PLOT2RST.current_figure +""" + +selem = morph.disk(5) +top_hat = morph.greyscale_white_top_hat(img_t, selem) + +imshow(top_hat) +plt.title("Tophat with disk of r = 5") + +""" +.. image:: PLOT2RST.current_figure +""" + +selem = morph.square(20) +opening = morph.greyscale_open(img_t, selem) +# scikit's top hat filter uses uint8 and doesn't check for over(under)flow. +mask = opening > img_t +opening[mask] = img_t[mask] +top_hat = img_t - opening + +imshow(opening, vmin=0, vmax=255) +plt.title("Greyscale opening of image") + +""" +.. image:: PLOT2RST.current_figure +""" + +imshow(top_hat) +plt.title("Tophat with square of w = 10") + +plt.show() + +""" +.. image:: PLOT2RST.current_figure + + +References +========== + +.. [1] Crocker and Grier, Journal of Colloid and Interface Science (1996) +.. [2] Vincent, L., IEEE Transactions on Image Processing (1993) + +""" diff --git a/skimage/data/noisy_circles.jpg b/skimage/data/noisy_circles.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a0f49d6767ca4ca3cfbef6c54c36039223d243ad GIT binary patch literal 26206 zcmeIa2UJr_*FSthBIOF9T@^x$B*3+R^b$&lAP{LPC@7*-A#_M+B81{axJp+6m7*X; znhHo$1BwDFy(!WJq?&+$L|XDc!E*2OywAVA-}=`2*7`hI=bR~f_UxJ2)ABpBnQxgN zgclA45zYaCsVT4*002&a8v+1O&;X?uv z;rsE%Ie9xcBagUxle~$p-abe@3l*fPy(7VkP7TRy-koJGfc4_pMeIXOAGIJvpF zxcRsLww-?mFE=;u4k1Co9fE>F{M=jD_t!Vizds>7+qd(;dHCRPzTe?+`0uO-{P%B3 z`2SBBFzW$+P9Ph|hC(C(Hhu_{AHp003;7$f5rPG!+y>dYK!I5o6o9b7wy|??a{aa) z^6!xlfDQU1k`I8uAW$|K+cpk%&TUX`B`}g73fnE9unl+AUQp6ELXrLVq}vY;qR=~z zH8?0K`$cx@aY!XwEPVLvNL1N*=V6qzeu|~jp=UWH)m>f3$D9|-FFx<~A73(%xtjZD z#kIzsiRHVMFM2{bwfffD#i3T{)3Ncx>6LBp}m1tqk@2fxT?{E|q*b1EfkNc$ayoQe zmE=73Q-uGy1Tue%z#IU$p<8+K16W`=Js*+RttVo8T&1M+5zto|xANeMOQ++hzIE$| z57%;T=9uu`R%*{$y=QBVa%$+AtE#_{s+5{`dST=uAMC=Hjh?F`<)t=?{Ta3mQ+m`u z@7X(!{+B&2MCILttpqx^Vy3E7#acf{4T>)okJ}Vvp!J)3rcvwoJyeDA!4EVfeeqN2 zl@ev-%Rha;`13tOCZ6LJP53f+EyKm#s7pj
  • F%n8k9it%ULC>PP!Y|GKDK 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 Q%i7-Ox?=fb6z04C0pd5rzW@LL literal 0 HcmV?d00001 diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index d639be1c..24982269 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -4,3 +4,4 @@ from .ccomp import label from .watershed import watershed, is_local_maximum from .skeletonize import skeletonize, medial_axis from .convex_hull import convex_hull_image +from .greyreconstruct import reconstruction diff --git a/skimage/morphology/_greyreconstruct.pyx b/skimage/morphology/_greyreconstruct.pyx new file mode 100644 index 00000000..18197812 --- /dev/null +++ b/skimage/morphology/_greyreconstruct.pyx @@ -0,0 +1,85 @@ +""" +`reconstruction_loop` originally part of CellProfiler, code licensed under both GPL and BSD licenses. + +Website: http://www.cellprofiler.org +Copyright (c) 2003-2009 Massachusetts Institute of Technology +Copyright (c) 2009-2011 Broad Institute +All rights reserved. +Original author: Lee Kamentsky + +""" + +from __future__ import division +import numpy as np + +cimport numpy as np +cimport cython + + +@cython.boundscheck(False) +def reconstruction_loop(np.ndarray[dtype=np.uint32_t, ndim=1, + negative_indices = False, + mode = 'c'] avalues, + np.ndarray[dtype=np.int32_t, ndim=1, + negative_indices = False, + mode = 'c'] aprev, + np.ndarray[dtype=np.int32_t, ndim=1, + negative_indices = False, + mode = 'c'] anext, + np.ndarray[dtype=np.int32_t, ndim=1, + negative_indices = False, + mode = 'c'] astrides, + np.int32_t current, + int image_stride): + """The inner loop for reconstruction""" + cdef: + np.int32_t neighbor + np.uint32_t neighbor_value + np.uint32_t current_value + np.uint32_t mask_value + np.int32_t link + int i + np.int32_t nprev + np.int32_t nnext + int nstrides = astrides.shape[0] + np.uint32_t *values = (avalues.data) + np.int32_t *prev = (aprev.data) + np.int32_t *next = (anext.data) + np.int32_t *strides = (astrides.data) + + while current != -1: + if current < image_stride: + current_value = values[current] + if current_value == 0: + break + for i in range(nstrides): + neighbor = current + strides[i] + neighbor_value = values[neighbor] + # Only do neighbors less than the current value + if neighbor_value < current_value: + mask_value = values[neighbor + image_stride] + # Only do neighbors less than the mask value + if neighbor_value < mask_value: + # Raise the neighbor to the mask value if + # the mask is less than current + if mask_value < current_value: + link = neighbor + image_stride + values[neighbor] = mask_value + else: + link = current + values[neighbor] = current_value + # unlink the neighbor + nprev = prev[neighbor] + nnext = next[neighbor] + next[nprev] = nnext + if nnext != -1: + prev[nnext] = nprev + # link the neighbor after the link + nnext = next[link] + next[neighbor] = nnext + prev[neighbor] = link + if nnext >= 0: + prev[nnext] = neighbor + next[link] = neighbor + current = next[current] + diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py new file mode 100644 index 00000000..7f0def38 --- /dev/null +++ b/skimage/morphology/greyreconstruct.py @@ -0,0 +1,150 @@ +""" +`reconstruction` originally part of CellProfiler, code licensed under both GPL and BSD licenses. + +Website: http://www.cellprofiler.org +Copyright (c) 2003-2009 Massachusetts Institute of Technology +Copyright (c) 2009-2011 Broad Institute +All rights reserved. +Original author: Lee Kamentsky + +""" +import numpy as np + +from skimage.filter.rank_order import rank_order + + +def reconstruction(image, mask, selem=None, offset=None): + """Perform a morphological reconstruction of the image. + + Reconstruction requires a "seed" image and a "mask" image. The seed image + gets dilated until it is constrained by the mask. The "seed" and "mask" + images will be the minimum and maximum possible values of the reconstructed + image. + + Parameters + ---------- + image : ndarray + The seed image. + + mask : ndarray + The maximum allowed value at each point. + + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + + Returns + ------- + reconstructed : ndarray + The result of morphological reconstruction. + + Notes + ----- + The algorithm is taken from: + Robinson, "Efficient morphological reconstruction: a downhill filter", + Pattern Recognition Letters 25 (2004) 1759-1767. + + Applications for greyscale reconstruction are discussed in: + Vincent, L., "Morphological Grayscale Reconstruction in Image Analysis: + Applications and Efficient Algorithms", IEEE Transactions on Image + Processing (1993) + + Examples + -------- + Uses for greyscale reconstruction are described in Vincent (1993). For + example, let's try to extract the features of an image by subtracting a + background image created by reconstruction. + + First, create an image where the "bumps" are the features that + we want to extract: + + >>> import numpy as np + >>> from scikits.image.morphology.grey import grey_reconstruction + >>> y, x = np.mgrid[:20:0.5, :20:0.5] + >>> bumps = np.sin(x) + np.sin(y) + + To create the background image, set the mask image to the original image, + and the seed image to the original image with an intensity offset, `h`. + + >>> h = 0.3 + >>> seed = bumps - h + >>> rec = grey_reconstruction(seed, bumps) + + The resulting reconstructed image looks exactly like the original image, + but with the peaks of the bumps cut off. Subtracting this reconstructed + image from the original image leaves just the peaks of the bumps + + >>> hdome = bumps - rec + + This operation is known as the h-dome of the image, which leaves features + of height `h` in the subtracted image. The h-dome transform, and its + inverse h-basin, are analogous to the white top-hat and black top-hat + transforms, but don't require a structuring element. + + """ + assert tuple(image.shape) == tuple(mask.shape) + assert np.all(image <= mask) + try: + from ._morphrec import reconstruction_loop + except ImportError: + raise ImportError("_morphrec extension not available.") + + if selem is None: + selem = np.ones([3]*image.ndim, bool) + else: + selem = selem.copy() + + if offset == None: + assert all([d % 2 == 1 for d in selem.shape]),\ + "Footprint dimensions must all be odd" + offset = np.array([d/2 for d in selem.shape]) + # Cross out the center of the selem + selem[[slice(d,d+1) for d in offset]] = False + # + # Construct an array that's padded on the edges so we can ignore boundaries + # The array is a dstack of the image and the mask; this lets us interleave + # image and mask pixels when sorting which makes list manipulations easier + # + padding = (np.array(selem.shape)/2).astype(int) + dims = np.zeros(image.ndim+1,int) + dims[1:] = np.array(image.shape)+2*padding + dims[0] = 2 + inside_slices = [slice(p,-p) for p in padding] + values = np.ones(dims)*np.min(image) + values[[0]+inside_slices] = image + values[[1]+inside_slices] = mask + # + # Create a list of strides across the array to get the neighbors + # within a flattened array + # + value_stride = np.array(values.strides[1:]) / values.dtype.itemsize + image_stride = values.strides[0] / values.dtype.itemsize + selem_mgrid = np.mgrid[[slice(-o,d - o) + for d,o in zip(selem.shape,offset)]] + selem_offsets = selem_mgrid[:,selem].transpose() + strides = np.array([np.sum(value_stride * selem_offset) + for selem_offset in selem_offsets], np.int32) + values = values.flatten() + value_sort = np.lexsort([-values]).astype(np.int32) + # + # Make a linked list of pixels sorted by value. -1 is the list terminator. + # + prev = -np.ones(len(values), np.int32) + next = -np.ones(len(values), np.int32) + prev[value_sort[1:]] = value_sort[:-1] + next[value_sort[:-1]] = value_sort[1:] + # + # Create a rank-order value array so that the Cython inner-loop + # can operate on a uniform data type + # + values, value_map = rank_order(values) + current = value_sort[0] + + reconstruction_loop(values, prev, next, strides, current, image_stride) + # + # Reshape the values array to the shape of the padded image + # and return the unpadded portion of that result + # + values = value_map[values[:image_stride]] + values.shape = np.array(image.shape)+2*padding + return values[inside_slices] + diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index fcf33f7f..fda13679 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -18,6 +18,7 @@ def configuration(parent_package='', top_path=None): cython(['_skeletonize.pyx'], working_path=base_path) cython(['_pnpoly.pyx'], working_path=base_path) cython(['_convex_hull.pyx'], working_path=base_path) + cython(['_greyreconstruct.pyx'], working_path=base_path) config.add_extension('ccomp', sources=['ccomp.c'], include_dirs=[get_numpy_include_dirs()]) @@ -31,6 +32,8 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('_convex_hull', sources=['_convex_hull.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_greyreconstruct', sources=['_greyreconstruct.c'], + include_dirs=[get_numpy_include_dirs()]) return config diff --git a/skimage/morphology/tests/test_reconstruction.py b/skimage/morphology/tests/test_reconstruction.py new file mode 100644 index 00000000..61b72033 --- /dev/null +++ b/skimage/morphology/tests/test_reconstruction.py @@ -0,0 +1,73 @@ +""" +These tests are originally part of CellProfiler, code licensed under both GPL and BSD licenses. + +Website: http://www.cellprofiler.org +Copyright (c) 2003-2009 Massachusetts Institute of Technology +Copyright (c) 2009-2011 Broad Institute +All rights reserved. +Original author: Lee Kamentsky +""" +import numpy as np + +from skimage.morphology.greyreconstruct import reconstruction + + +def test_zeros(): + """Test reconstruction with image and mask of zeros""" + assert np.all(reconstruction(np.zeros((5, 7)), np.zeros((5, 7))) == 0) + + +def test_image_equals_mask(): + """Test reconstruction where the image and mask are the same""" + assert np.all(reconstruction(np.ones((7, 5)), np.ones((7, 5))) == 1) + + +def test_image_less_than_mask(): + """Test reconstruction where the image is uniform and less than mask""" + image = np.ones((5, 5)) + mask = np.ones((5, 5)) * 2 + assert np.all(reconstruction(image,mask) == 1) + + +def test_one_image_peak(): + """Test reconstruction with one peak pixel""" + image = np.ones((5, 5)) + image[2, 2] = 2 + mask = np.ones((5, 5)) * 3 + assert np.all(reconstruction(image,mask) == 2) + + +def test_two_image_peaks(): + """Test reconstruction with two peak pixels isolated by the mask""" + image = np.array([[1, 1, 1, 1, 1, 1, 1, 1], + [1, 2, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 3, 1], + [1, 1, 1, 1, 1, 1, 1, 1]]) + + mask = np.array([[4, 4, 4, 1, 1, 1, 1, 1], + [4, 4, 4, 1, 1, 1, 1, 1], + [4, 4, 4, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 4, 4, 4], + [1, 1, 1, 1, 1, 4, 4, 4], + [1, 1, 1, 1, 1, 4, 4, 4]]) + + expected = np.array([[2, 2, 2, 1, 1, 1, 1, 1], + [2, 2, 2, 1, 1, 1, 1, 1], + [2, 2, 2, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 3, 3, 3], + [1, 1, 1, 1, 1, 3, 3, 3], + [1, 1, 1, 1, 1, 3, 3, 3]]) + assert np.all(reconstruction(image,mask) == expected) + + +def test_zero_image_one_mask(): + """Test reconstruction with an image of all zeros and a mask that's not""" + result = reconstruction(np.zeros((10, 10)), np.ones((10, 10))) + assert np.all(result == 0) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() From c0c23968bf7cd893e28c9f771b0aa97ff1d48e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Sat, 14 Jul 2012 14:14:28 +0200 Subject: [PATCH 016/648] add perimeter measurement --- skimage/measure/__init__.py | 2 +- skimage/measure/_regionprops.py | 62 +++++++++++++++++++++-- skimage/measure/tests/test_regionprops.py | 4 ++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 588ec74d..f5109698 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -1,3 +1,3 @@ from .find_contours import find_contours -from ._regionprops import regionprops +from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 6b8f6a3c..396c6bdd 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -10,6 +10,9 @@ from . import _moments __all__ = ['regionprops'] +STREL_4 = np.array([[0, 1, 0], + [1, 1, 1], + [0, 1, 0]]) STREL_8 = np.ones((3, 3), 'int8') PROPS = ( 'Area', @@ -36,7 +39,7 @@ PROPS = ( 'Moments', 'NormalizedMoments', 'Orientation', -# 'Perimeter', + 'Perimeter', # 'PixelIdxList', # 'PixelList', 'Solidity', @@ -122,6 +125,9 @@ def regionprops(label_image, properties=['Area', 'Centroid'], Angle between the X-axis and the major axis of the ellipse that has the same second-moments as the region. Ranging from `-pi/2` to `-pi/2` in counter-clockwise direction. + * Perimeter : float + Perimeter of object which approximates the contour as a line through + the centers of border pixels using a 4-connectivity. * Solidity : float Ratio of pixels in the region to pixels of the convex hull image. * WeightedCentralMoments : 3 x 3 ndarray @@ -210,8 +216,8 @@ def regionprops(label_image, properties=['Area', 'Centroid'], b = mu[1, 1] / mu[0, 0] c = mu[0, 2] / mu[0, 0] #: eigen values of inertia tensor - l1 = (a + c) / 2 + sqrt(4 * b**2 + (a - c)**2) / 2 - l2 = (a + c) / 2 - sqrt(4 * b**2 + (a - c)**2) / 2 + l1 = (a + c) / 2 + sqrt(4 * b ** 2 + (a - c) ** 2) / 2 + l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 # cached results which are used by several properties _filled_image = None @@ -297,6 +303,9 @@ def regionprops(label_image, properties=['Area', 'Centroid'], else: obj_props['Orientation'] = - 0.5 * atan(2 * b / (a - c)) + if 'Perimeter' in properties: + obj_props['Perimeter'] = perimeter(array, 4) + if 'Solidity' in properties: if _convex_image is None: _convex_image = convex_hull_image(array) @@ -350,3 +359,50 @@ def regionprops(label_image, properties=['Area', 'Centroid'], obj_props['WeightedNormalizedMoments'] = _wnu return props + + +def perimeter(image, neighbourhood=4): + """Calculate total perimeter of all objects in binary image. + + Parameters + ---------- + image : array + binary image + neighbourhood: 4 or 8, optional + neighbourhood connectivity for border pixel determination, default 4 + + Returns + ------- + perimeter : float + total perimeter of all objects in binary image + + References + ---------- + K. Benkrid, D. Crookes. Design and FPGA Implementation of a Perimeter + Estimator. The Queen's University of Belfast. + http://www.cs.qub.ac.uk/~d.crookes/webpubs/papers/perimeter.doc + """ + if neighbourhood == 4: + strel = STREL_4 + else: + strel = STREL_8 + eroded_image = ndimage.binary_erosion(image, strel) + border_image = image - eroded_image + + # perimeter contribution: corresponding values in convolved image + perimeter_weights = { + 1: (5, 7, 15, 17, 25, 27), + sqrt(2): (21, 33), + 1 + sqrt(2) / 2: (13, 23) + } + perimeter_image = ndimage.convolve(border_image, np.array([[10, 2, 10], + [ 2, 1, 2], + [10, 2, 10]])) + total_perimeter = 0 + for weight, values in perimeter_weights.items(): + num_values = 0 + for value in values: + num_values += np.sum(perimeter_image == value) + total_perimeter += num_values * weight + + return total_perimeter diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index b9b16dff..4408c3d1 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -194,6 +194,10 @@ def test_orientation(): # determined with MATLAB assert_almost_equal(orientation, 0.10446844651921) +def test_perimeter(): + perimeter = regionprops(SAMPLE, ['Perimeter'])[0]['Perimeter'] + assert_almost_equal(perimeter, 59.2132034355964) + def test_solidity(): solidity = regionprops(SAMPLE, ['Solidity'])[0]['Solidity'] # determined with MATLAB From 84e18de02da8f01907e30bb0ffe1ded7e006a095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Mon, 4 Jun 2012 15:22:30 +0200 Subject: [PATCH 017/648] improve and restructure geomtric transformations --- skimage/transform/__init__.py | 3 +- skimage/transform/_warp.py | 113 ----- skimage/transform/_warp_zoo.py | 75 --- skimage/transform/geometric.py | 558 ++++++++++++++++++++++ skimage/transform/project.py | 137 ------ skimage/transform/tests/test_geometric.py | 146 ++++++ skimage/transform/tests/test_project.py | 63 --- skimage/transform/tests/test_swirl.py | 17 - 8 files changed, 705 insertions(+), 407 deletions(-) delete mode 100644 skimage/transform/_warp.py delete mode 100644 skimage/transform/_warp_zoo.py create mode 100644 skimage/transform/geometric.py delete mode 100644 skimage/transform/project.py create mode 100644 skimage/transform/tests/test_geometric.py delete mode 100644 skimage/transform/tests/test_project.py delete mode 100644 skimage/transform/tests/test_swirl.py diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index fa4059ee..f5621444 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -4,5 +4,4 @@ from .finite_radon_transform import * from .project import * from ._project import homography as fast_homography from .integral import * -from ._warp import warp -from ._warp_zoo import swirl +from .geometric import warp, make_tform, swirl, homography diff --git a/skimage/transform/_warp.py b/skimage/transform/_warp.py deleted file mode 100644 index 6477c988..00000000 --- a/skimage/transform/_warp.py +++ /dev/null @@ -1,113 +0,0 @@ -__all__ = ['warp'] - -import numpy as np -from scipy import ndimage -from skimage.util import img_as_float - - -def _stackcopy(a, b): - """Copy b into each color layer of a, such that:: - - a[:,:,0] = a[:,:,1] = ... = b - - Parameters - ---------- - a : (M, N) or (M, N, P) ndarray - Target array. - b : (M, N) - Source array. - - Notes - ----- - Color images are stored as an ``MxNx3`` or ``MxNx4`` arrays. - - """ - if a.ndim == 3: - a[:] = b[:, :, np.newaxis] - else: - a[:] = b - - -def warp(image, reverse_map, map_args={}, - output_shape=None, order=1, mode='constant', cval=0.): - """Warp an image according to a given coordinate transformation. - - Parameters - ---------- - image : 2-D array - Input image. - reverse_map : callable xy = f(xy, **kwargs) - Reverse coordinate map. A function that transforms a Px2 array of - ``(x, y)`` coordinates in the *output image* into their corresponding - coordinates in the *source image*. Also see examples below. - map_args : dict, optional - Keyword arguments passed to `reverse_map`. - output_shape : tuple (rows, cols) - Shape of the output image generated. - order : int - Order of splines used in interpolation. See - `scipy.ndimage.map_coordinates` for detail. - mode : string - How to handle values outside the image borders. See - `scipy.ndimage.map_coordinates` for detail. - cval : string - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - Examples - -------- - Shift an image to the right: - - >>> from skimage import data - >>> image = data.camera() - >>> - >>> def shift_right(xy): - ... xy[:, 0] -= 10 - ... return xy - >>> - >>> warp(image, shift_right) - - """ - if image.ndim < 2: - raise ValueError("Input must have more than 1 dimension.") - - image = np.atleast_3d(img_as_float(image)) - ishape = np.array(image.shape) - bands = ishape[2] - - if output_shape is None: - output_shape = ishape - - coords = np.empty(np.r_[3, output_shape], dtype=float) - - ## Construct transformed coordinates - - rows, cols = output_shape[:2] - - # Reshape grid coordinates into a (P, 2) array of (x, y) pairs - tf_coords = np.indices((cols, rows), dtype=float).reshape(2, -1).T - - # Map each (x, y) pair to the source image according to - # the user-provided mapping - tf_coords = reverse_map(tf_coords, **map_args) - - # Reshape back to a (2, M, N) coordinate grid - tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2) - - # Place the y-coordinate mapping - _stackcopy(coords[1, ...], tf_coords[0, ...]) - - # Place the x-coordinate mapping - _stackcopy(coords[0, ...], tf_coords[1, ...]) - - # colour-coordinate mapping - coords[2, ...] = range(bands) - - # Prefilter not necessary for order 1 interpolation - prefilter = order > 1 - mapped = ndimage.map_coordinates(image, coords, prefilter=prefilter, - mode=mode, order=order, cval=cval) - - # The spline filters sometimes return results outside [0, 1], - # so clip to ensure valid data - return np.clip(mapped.squeeze(), 0, 1) diff --git a/skimage/transform/_warp_zoo.py b/skimage/transform/_warp_zoo.py deleted file mode 100644 index 4df531d1..00000000 --- a/skimage/transform/_warp_zoo.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import division -import numpy as np - -from ._warp import warp - - -def _swirl_mapping(xy, center, rotation, strength, radius): - x, y = xy.T - x0, y0 = center - rho = np.sqrt((x - x0)**2 + (y - y0)**2) - - # Ensure that the transformation decays to approximately 1/1000-th - # within the specified radius. - radius = radius / 5 * np.log(2) - - theta = rotation + strength * \ - np.exp(-rho / radius) + \ - np.arctan2(y - y0, x - x0) - - xy[..., 0] = x0 + rho * np.cos(theta) - xy[..., 1] = y0 + rho * np.sin(theta) - - return xy - - -def swirl(image, center=None, strength=1, radius=100, rotation=0, - output_shape=None, order=1, mode='constant', cval=0): - """Perform a swirl transformation. - - Parameters - ---------- - image : ndarray - Input image. - center : (x,y) tuple or (2,) ndarray - Center coordinate of transformation. - strength : float - The amount of swirling applied. - radius : float - The extent of the swirl in pixels. The effect dies out - rapidly beyond `radius`. - rotation : float - Additional rotation applied to the image. - - Returns - ------- - swirled : ndarray - Swirled version of the input. - - Other parameters - ---------------- - output_shape : tuple or ndarray - Size of the generated output image. - order : int - Order of splines used in interpolation. See - `scipy.ndimage.map_coordinates` for detail. - mode : string - How to handle values outside the image borders. See - `scipy.ndimage.map_coordinates` for detail. - cval : string - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - """ - - if center is None: - center = np.array(image.shape)[:2] / 2 - - warp_args = {'center': center, - 'rotation': rotation, - 'strength': strength, - 'radius': radius} - - return warp(image, _swirl_mapping, map_args=warp_args, - output_shape=output_shape, - order=order, mode=mode, cval=cval) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py new file mode 100644 index 00000000..0306af59 --- /dev/null +++ b/skimage/transform/geometric.py @@ -0,0 +1,558 @@ +# coding: utf-8 +import math +import numpy as np +from scipy import ndimage +from skimage.util import img_as_float + + +EPS = np.spacing(1) + + +def _stackcopy(a, b): + """Copy b into each color layer of a, such that:: + + a[:,:,0] = a[:,:,1] = ... = b + + Parameters + ---------- + a : (M, N) or (M, N, P) ndarray + Target array. + b : (M, N) + Source array. + + Notes + ----- + Color images are stored as an ``MxNx3`` or ``MxNx4`` arrays. + + """ + if a.ndim == 3: + a[:] = b[:, :, np.newaxis] + else: + a[:] = b + +def _make_similarity(src, dst): + """Determine parameters of the 2D similarity transformation: + X = a0*x - b0*y + a1 + Y = b0*x + a0*y + a2 + where the homogeneous transformation matrix is: + [[a1 -b1 a0] + [b1 a1 b0] + [0 0 1]] + """ + xs = src[:,0] + ys = src[:,1] + xd = dst[:,0] + yd = dst[:,1] + rows = src.shape[0] + + A = np.zeros((rows*2, 4)) + b = np.zeros((rows*2,)) + + A[:rows,0] = xs + A[:rows,2] = - ys + A[:rows,1] = 1 + A[rows:,2] = xs + A[rows:,0] = ys + A[rows:,3] = 1 + b[:rows] = xd + b[rows:] = yd + + a0, a1, b0, b1 = np.linalg.lstsq(A, b)[0] + matrix = np.eye(3) + matrix[0,0] = a0 + matrix[0,1] = - b0 + matrix[0,2] = a1 + matrix[1,0] = b0 + matrix[1,1] = a0 + matrix[1,2] = b1 + return matrix + +def _make_affine(src, dst): + """Determine parameters of the 2D affine transformation: + X = a0*x + a1*y + a3 + Y = b0*x + b1*y + b3 + where the homogeneous transformation matrix is: + [[a0 a1 a2] + [b0 b1 b2] + [0 0 1]] + """ + xs = src[:,0] + ys = src[:,1] + xd = dst[:,0] + yd = dst[:,1] + rows = src.shape[0] + + A = np.zeros((rows*2, 6)) + b = np.zeros((rows*2,)) + + A[:rows,0] = xs + A[:rows,1] = ys + A[:rows,2] = 1 + A[rows:,3] = xs + A[rows:,4] = ys + A[rows:,5] = 1 + + b[:rows] = xd + b[rows:] = yd + + params = np.linalg.lstsq(A, b)[0] + matrix = np.eye(3) + matrix[:2,:] = params.reshape((2, 3)) + return matrix + +def _make_projective(src, dst): + """Determine transformation matrix of the 2D projective transformation: + X = (a0 + a1*x + a2*y) / (c0*x + c1*y + c3) + Y = (b0 + b1*x + b2*y) / (c0*x + c1*y + c3) + where the homogeneous transformation matrix is: + [[a0 a1 a2] + [b0 b1 b2] + [c0 c1 c3]] + """ + xs = src[:,0] + ys = src[:,1] + xd = dst[:,0] + yd = dst[:,1] + rows = src.shape[0] + + A = np.zeros((rows*2, 8)) + b = np.zeros((rows*2,)) + + + A[:rows,0] = xs + A[:rows,1] = ys + A[:rows,2] = 1 + A[:rows,6] = - xd * xs + A[:rows,7] = - xd * ys + A[rows:,3] = xs + A[rows:,4] = ys + A[rows:,5] = 1 + A[rows:,6] = - yd * xs + A[rows:,7] = - yd * ys + b[:rows] = dst[:,0] + b[rows:] = dst[:,1] + + matrix = np.eye(3).flatten() + matrix[:8] = np.linalg.lstsq(A, b)[0] + return matrix.reshape((3, 3)) + +def _make_polynomial(src, dst, order): + """Determine parameters of 2D polynomial transformation of order n: + X = sum[j=0:n]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) + Y = sum[j=0:n]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) + """ + xs = src[:,0] + ys = src[:,1] + xd = dst[:,0] + yd = dst[:,1] + rows = src.shape[0] + + # number of unknown polynomial coefficients + u = (order + 1) * (order + 2) + A = np.zeros((rows*2, u)) + b = np.zeros((rows*2,)) + + pidx = 0 + for j in xrange(order+1): + for i in xrange(j+1): + A[:rows,pidx] = xs ** (j - i) * ys ** i + A[rows:,pidx+u/2] = xs ** (j - i) * ys ** i + pidx += 1 + b[:rows] = xd + b[rows:] = yd + + return np.linalg.lstsq(A, b)[0] + +def _make_rotation(angle): + """Determine homogeneous transformation matrix of 2D rotation: + [[cos(angle) -sin(angle) 0] + [sin(angle) cos(angle) 0] + [0 0 1]] + """ + R = [ + [math.cos(angle), -math.sin(angle), 0], + [math.sin(angle), math.cos(angle), 0], + [0, 0, 1], + ] + return np.array(R) + +def _transform(coords, matrix): + src = np.vstack((coords[:,0], coords[:,1], np.ones((coords.shape[0],)))) + dst = np.dot(src.transpose(), matrix.transpose()) + # rescale to homogeneous coordinates + dst[:,0] *= 1 / dst[:,2] + dst[:,1] *= 1 / dst[:,2] + dst[np.abs(dst) < EPS] = 0 + return dst[:,:2] + +def _transform_polynomial(coords, matrix): + x = coords[:,0] + y = coords[:,1] + u = len(matrix) + # number of coefficients -> u = (order + 1) * (order + 2) + order = int((-3 + math.sqrt(9 - 4 * (2 - u))) / 2) + dst = np.zeros(coords.shape) + + pidx = 0 + for j in xrange(order+1): + for i in xrange(j+1): + dst[:,0] += matrix[pidx] * x ** (j - i) * y ** i + dst[:,1] += matrix[pidx+u/2] * x ** (j - i) * y ** i + pidx += 1 + + return dst + + +TRANSFORMATIONS = { + 'similarity': (_make_similarity, _transform), + 'affine': (_make_affine, _transform), + 'projective': (_make_projective, _transform), + 'polynomial': (_make_polynomial, _transform_polynomial), + 'rotation': (_make_rotation, _transform), +} + + +class Transformation(object): + + def __init__(self, ttype, matrix): + """Create transformation which contains the transformation parameters + and can perform forward and inverse transformations. + + Parameters + ---------- + ttype : str + one of similarity, affine, projective, polynomial, rotation + matrix : 3x3 array + homogeneous transformation matrix + """ + self.ttype = ttype + self.matrix = matrix + + def fwd(self, coords): + """Apply forward transformation. + + Parameters + ---------- + coords : Nx2 array + source coordinates + + Returns + ------- + coords : Nx2 array + transformed coordinates + """ + return TRANSFORMATIONS[self.ttype][1](coords, self.matrix) + + def inv(self, coords): + """Apply inverse transformation. + + Parameters + ---------- + coords : Nx2 array + source coordinates + + Returns + ------- + coords : Nx2 array + transformed coordinates + """ + if self.ttype == 'polynomial': + raise Exception( + 'There is no explicit way to do the inverse polynomial ' + 'transformation. Instead determine the inverse transformation ' + 'parameters and use the forward transformation instead.') + matrix = np.linalg.inv(self.matrix) + return TRANSFORMATIONS[self.ttype][1](coords, matrix) + + +def make_tform(ttype, **kwargs): + """Create geometric transformation. + + You can determine the over-, well- and under-determined parameters + with the least-squares method. + + + + Number of source must match number of destination coordinates. + + Parameters + ---------- + ttype : str + one of similarity, affine, projective, polynomial, rotation + kwargs : array or int + function parameters (src, dst, n, angle): + + NAME / TTYPE FUNCTION PARAMETERS + 'similarity' `src, `dst` + 'affine' `src, `dst` + 'projective' `src, `dst` + 'polynomial' `src, `dst`, `order` (polynomial order) + 'rotation' `angle` + + Alternatively you can explicitly define a 3x3 homogeneous transformation + matrix with the `matrix` parameter. + + See examples section below for usage. + + Returns + ------- + tform : :class:`Transformation` + tform object containing the transformation parameters + """ + + ttype = ttype.lower() + if ttype not in TRANSFORMATIONS: + raise NotImplemented('the transformation type \'%s\' is not' + 'implemented' % ttype) + if 'matrix' in kwargs: + matrix = kwargs['matrix'] + else: + matrix = TRANSFORMATIONS[ttype][0](**kwargs) + return Transformation(ttype, matrix) + +def warp(image, reverse_map=None, map_args={}, tform=None, + output_shape=None, order=1, mode='constant', cval=0.): + """Warp an image according to a given coordinate transformation. + + Parameters + ---------- + image : 2-D array + Input image. + reverse_map : callable xy = f(xy, **kwargs) + Reverse coordinate map. A function that transforms a Px2 array of + ``(x, y)`` coordinates in the *output image* into their corresponding + coordinates in the *source image*. Also see examples below. + map_args : dict, optional + Keyword arguments passed to `reverse_map`. + tform : :class:`Transformation` object + The inverse transformation will be used to transform coordinates in the + *output image* into their corresponding coordinates in the + *source image*. + output_shape : tuple (rows, cols) + Shape of the output image generated. + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Examples + -------- + Shift an image to the right: + + >>> from skimage import data + >>> image = data.camera() + >>> + >>> def shift_right(xy): + ... xy[:, 0] -= 10 + ... return xy + >>> + >>> warp(image, shift_right) + + """ + if image.ndim < 2: + raise ValueError("Input must have more than 1 dimension.") + + image = np.atleast_3d(img_as_float(image)) + ishape = np.array(image.shape) + bands = ishape[2] + + if output_shape is None: + output_shape = ishape + + coords = np.empty(np.r_[3, output_shape], dtype=float) + + ## Construct transformed coordinates + + rows, cols = output_shape[:2] + + # Reshape grid coordinates into a (P, 2) array of (x, y) pairs + tf_coords = np.indices((cols, rows), dtype=float).reshape(2, -1).T + + # Map each (x, y) pair to the source image according to + # the user-provided mapping + if callable(reverse_map): + tf_coords = reverse_map(tf_coords, **map_args) + else: + tf_coords = tform.inv(tf_coords) + + # Reshape back to a (2, M, N) coordinate grid + tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2) + + # Place the y-coordinate mapping + _stackcopy(coords[1, ...], tf_coords[0, ...]) + + # Place the x-coordinate mapping + _stackcopy(coords[0, ...], tf_coords[1, ...]) + + # colour-coordinate mapping + coords[2, ...] = range(bands) + + # Prefilter not necessary for order 1 interpolation + prefilter = order > 1 + mapped = ndimage.map_coordinates(image, coords, prefilter=prefilter, + mode=mode, order=order, cval=cval) + + # The spline filters sometimes return results outside [0, 1], + # so clip to ensure valid data + return np.clip(mapped.squeeze(), 0, 1) + +def _swirl_mapping(xy, center, rotation, strength, radius): + x, y = xy.T + x0, y0 = center + rho = np.sqrt((x - x0)**2 + (y - y0)**2) + + # Ensure that the transformation decays to approximately 1/1000-th + # within the specified radius. + radius = radius / 5 * np.log(2) + + theta = rotation + strength * \ + np.exp(-rho / radius) + \ + np.arctan2(y - y0, x - x0) + + xy[..., 0] = x0 + rho * np.cos(theta) + xy[..., 1] = y0 + rho * np.sin(theta) + + return xy + +def swirl(image, center=None, strength=1, radius=100, rotation=0, + output_shape=None, order=1, mode='constant', cval=0): + """Perform a swirl transformation. + + Parameters + ---------- + image : ndarray + Input image. + center : (x,y) tuple or (2,) ndarray + Center coordinate of transformation. + strength : float + The amount of swirling applied. + radius : float + The extent of the swirl in pixels. The effect dies out + rapidly beyond `radius`. + rotation : float + Additional rotation applied to the image. + + Returns + ------- + swirled : ndarray + Swirled version of the input. + + Other parameters + ---------------- + output_shape : tuple or ndarray + Size of the generated output image. + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + """ + + if center is None: + center = np.array(image.shape)[:2] / 2 + + warp_args = {'center': center, + 'rotation': rotation, + 'strength': strength, + 'radius': radius} + + return warp(image, _swirl_mapping, map_args=warp_args, + output_shape=output_shape, + order=order, mode=mode, cval=cval) + +def homography(image, H, output_shape=None, order=1, + mode='constant', cval=0.): + """Perform a projective transformation (homography) on an image. + + For each pixel, given its homogeneous coordinate :math:`\mathbf{x} + = [x, y, 1]^T`, its target position is calculated by multiplying + with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`. + E.g., to rotate by theta degrees clockwise, the matrix should be + + :: + + [[cos(theta) -sin(theta) 0] + [sin(theta) cos(theta) 0] + [0 0 1]] + + or, to translate x by 10 and y by 20, + + :: + + [[1 0 10] + [0 1 20] + [0 0 1 ]]. + + Parameters + ---------- + image : 2-D array + Input image. + H : array of shape ``(3, 3)`` + Transformation matrix H that defines the homography. + output_shape : tuple (rows, cols) + Shape of the output image generated. + order : int + Order of splines used in interpolation. + mode : string + How to handle values outside the image borders. Passed as-is + to ndimage. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Examples + -------- + >>> # rotate by 90 degrees around origin and shift down by 2 + >>> x = np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1 + >>> x + array([[1, 2, 3], + [4, 5, 6], + [7, 8, 9]], dtype=uint8) + >>> theta = -np.pi/2 + >>> M = np.array([[np.cos(theta),-np.sin(theta),0], + ... [np.sin(theta), np.cos(theta),2], + ... [0, 0, 1]]) + >>> x90 = homography(x, M, order=1) + >>> x90 + array([[3, 6, 9], + [2, 5, 8], + [1, 4, 7]], dtype=uint8) + >>> # translate right by 2 and down by 1 + >>> y = np.zeros((5,5), dtype=np.uint8) + >>> y[1, 1] = 255 + >>> y + array([[ 0, 0, 0, 0, 0], + [ 0, 255, 0, 0, 0], + [ 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0]], dtype=uint8) + >>> M = np.array([[ 1., 0., 2.], + ... [ 0., 1., 1.], + ... [ 0., 0., 1.]]) + >>> y21 = homography(y, M, order=1) + >>> y21 + array([[ 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0], + [ 0, 0, 0, 255, 0], + [ 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0]], dtype=uint8) + + """ + import warnings + warnings.warn('the homography function is deprecated; ' + 'use the `warp` and `tform` function instead', + category=DeprecationWarning) + + tform = make_tform('projective', matrix=H) + return warp(image, tform=tform, output_shape=output_shape, order=order, + mode=mode, cval=cval) diff --git a/skimage/transform/project.py b/skimage/transform/project.py deleted file mode 100644 index a140e03e..00000000 --- a/skimage/transform/project.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Image projection. - -""" - -import numpy as np -from scipy.ndimage import interpolation as ndii -from ._warp import _stackcopy - -__all__ = ['homography'] - -eps = np.finfo(float).eps - - -def homography(image, H, output_shape=None, order=1, - mode='constant', cval=0.): - """Perform a projective transformation (homography) on an image. - - For each pixel, given its homogeneous coordinate :math:`\mathbf{x} - = [x, y, 1]^T`, its target position is calculated by multiplying - with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`. - E.g., to rotate by theta degrees clockwise, the matrix should be - - :: - - [[cos(theta) -sin(theta) 0] - [sin(theta) cos(theta) 0] - [0 0 1]] - - or, to translate x by 10 and y by 20, - - :: - - [[1 0 10] - [0 1 20] - [0 0 1 ]]. - - Parameters - ---------- - image : 2-D array - Input image. - H : array of shape ``(3, 3)`` - Transformation matrix H that defines the homography. - output_shape : tuple (rows, cols) - Shape of the output image generated. - order : int - Order of splines used in interpolation. - mode : string - How to handle values outside the image borders. Passed as-is - to ndimage. - cval : string - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - Examples - -------- - >>> # rotate by 90 degrees around origin and shift down by 2 - >>> x = np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1 - >>> x - array([[1, 2, 3], - [4, 5, 6], - [7, 8, 9]], dtype=uint8) - >>> theta = -np.pi/2 - >>> M = np.array([[np.cos(theta),-np.sin(theta),0], - ... [np.sin(theta), np.cos(theta),2], - ... [0, 0, 1]]) - >>> x90 = homography(x, M, order=1) - >>> x90 - array([[3, 6, 9], - [2, 5, 8], - [1, 4, 7]], dtype=uint8) - >>> # translate right by 2 and down by 1 - >>> y = np.zeros((5,5), dtype=np.uint8) - >>> y[1, 1] = 255 - >>> y - array([[ 0, 0, 0, 0, 0], - [ 0, 255, 0, 0, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0]], dtype=uint8) - >>> M = np.array([[ 1., 0., 2.], - ... [ 0., 1., 1.], - ... [ 0., 0., 1.]]) - >>> y21 = homography(y, M, order=1) - >>> y21 - array([[ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 255, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0]], dtype=uint8) - - """ - if image.ndim < 2: - raise ValueError("Input must have more than 1 dimension.") - - image = np.atleast_3d(image) - ishape = np.array(image.shape) - bands = ishape[2] - - if output_shape is None: - output_shape = ishape - - coords = np.empty(np.r_[3, output_shape], dtype=float) - - # TODO: Refactor this method to use transform.warp instead. - - # Construct transformed coordinates - rows, cols = output_shape[:2] - rows, cols = np.mgrid[:rows, :cols] - tf_coords = np.empty(shape=cols.shape, - dtype=[('cols', float), - ('rows', float), - ('z', float)]) - tf_coords['cols'], tf_coords['rows'] = cols, rows - tf_coords['z'] = 1 - tf_coords = tf_coords.view((float, 3)) - - tf_coords = np.dot(tf_coords, np.linalg.inv(H).transpose()) - tf_coords[np.absolute(tf_coords) < eps] = 0. - - # normalize coordinates - tf_coords[..., :2] /= tf_coords[..., 2, np.newaxis] - - # y-coordinate mapping - _stackcopy(coords[0, ...], tf_coords[..., 1]) - - # x-coordinate mapping - _stackcopy(coords[1, ...], tf_coords[..., 0]) - - # colour-coordinate mapping - coords[2, ...] = range(bands) - - # Prefilter not necessary for order 1 interpolation - prefilter = order > 1 - mapped = ndii.map_coordinates(image, coords, prefilter=prefilter, - mode=mode, order=order, cval=cval) - - return mapped.squeeze() diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py new file mode 100644 index 00000000..a227f597 --- /dev/null +++ b/skimage/transform/tests/test_geometric.py @@ -0,0 +1,146 @@ +import numpy as np +from numpy.testing import assert_array_almost_equal + +from skimage.transform.geometric import _stackcopy +from skimage.transform import make_tform +from skimage.transform import homography, fast_homography +from skimage import transform as tf, data, img_as_float +from skimage.color import rgb2gray + + +SRC = np.array([ + [-12.3705, -10.5075], + [-10.7865, 15.4305], + [8.6985, 10.8675], + [11.4975, -9.5715], + [7.8435, 7.4835], + [-5.3325, 6.5025], + [6.7905, -6.3765], + [-6.1695, -0.8235], +]) +DST = np.array([ + [0, 0], + [0, 5800], + [4900, 5800], + [4900, 0], + [4479, 4580], + [1176, 3660], + [3754, 790], + [1024, 1931], +]) + + +def test_stackcopy(): + layers = 4 + x = np.empty((3, 3, layers)) + y = np.eye(3, 3) + _stackcopy(x, y) + for i in range(layers): + assert_array_almost_equal(x[...,i], y) + +def test_similarity(): + #: exact solution + tform = make_tform('similarity', src=SRC[:2,:], dst=DST[:2,:]) + assert_array_almost_equal(tform.fwd(SRC[:2,:]), DST[:2,:]) + assert_array_almost_equal(tform.inv(tform.fwd(SRC)), SRC) + + #: over-determined + tform = make_tform('similarity', src=SRC, dst=DST) + ref = np.array( + [[2.3632898110e+02, -5.5876792257e+00, 2.5331569391e+03], + [5.5876792257e+00, 2.3632898110e+02, 2.4358232635e+03], + [0.0000000000e+00, 0.0000000000e+00, 1.0000000000e+00]]) + assert_array_almost_equal(tform.matrix, ref) + assert_array_almost_equal(tform.inv(tform.fwd(SRC)), SRC) + +def test_affine(): + #: exact solution + tform = make_tform('affine', src=SRC[:3,:], dst=DST[:3,:]) + assert_array_almost_equal(tform.fwd(SRC[:3,:]), DST[:3,:]) + assert_array_almost_equal(tform.inv(tform.fwd(SRC)), SRC) + + #: over-determined + tform = make_tform('affine', src=SRC, dst=DST) + ref = np.array( + [[2.2573930047e+02, 7.1588596765e+00, 2.5126622012e+03], + [2.1234856855e+01, 2.4931019555e+02, 2.4143862183e+03], + [0.0000000000e+00, 0.0000000000e+00, 1.0000000000e+00]]) + assert_array_almost_equal(tform.matrix, ref) + assert_array_almost_equal(tform.inv(tform.fwd(SRC)), SRC) + +def test_projective(): + #: exact solution + tform = make_tform('projective', src=SRC[:4,:], dst=DST[:4,:]) + ref = np.array( + [[ 1.9466901291e+02, -1.1888183994e+01, 2.2832379309e+03], + [ -8.6910077540e+00, 2.2162069773e+02, 2.2211673699e+03], + [ -1.2695966735e-02, -9.6053624285e-03, 1.0000000000e+00]]) + assert_array_almost_equal(tform.matrix, ref, 6) + assert_array_almost_equal(tform.inv(tform.fwd(SRC)), SRC) + + #: over-determined + tform = make_tform('projective', src=SRC[:4,:], dst=DST[:4,:]) + ref = np.array( + [[ 1.9466901291e+02, -1.1888183994e+01, 2.2832379309e+03], + [ -8.6910077540e+00, 2.2162069773e+02, 2.2211673699e+03], + [ -1.2695966735e-02, -9.6053624285e-03, 1.0000000000e+00]]) + assert_array_almost_equal(tform.matrix, ref, 6) + assert_array_almost_equal(tform.inv(tform.fwd(SRC)), SRC) + +def test_polynomial(): + tform = make_tform('polynomial', src=SRC, dst=DST, order=10) + assert_array_almost_equal(tform.fwd(SRC), DST, 6) + +def test_homography(): + x = img_as_float(np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1) + theta = -np.pi/2 + M = np.array([[np.cos(theta),-np.sin(theta),0], + [np.sin(theta), np.cos(theta),2], + [0, 0, 1]]) + x90 = homography(x, M, order=1) + assert_array_almost_equal(x90, np.rot90(x)) + +def test_fast_homography(): + img = rgb2gray(data.lena()).astype(np.uint8) + img = img[:, :100] + + theta = np.deg2rad(30) + scale = 0.5 + tx, ty = 50, 50 + + H = np.eye(3) + S = scale * np.sin(theta) + C = scale * np.cos(theta) + + H[:2, :2] = [[C, -S], [S, C]] + H[:2, 2] = [tx, ty] + + for mode in ('constant', 'mirror', 'wrap'): + p0 = homography(img, H, mode=mode, order=1) + p1 = fast_homography(img, H, mode=mode) + p1 = np.round(p1) + + ## import matplotlib.pyplot as plt + ## f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4) + ## ax0.imshow(img) + ## ax1.imshow(p0, cmap=plt.cm.gray) + ## ax2.imshow(p1, cmap=plt.cm.gray) + ## ax3.imshow(np.abs(p0 - p1), cmap=plt.cm.gray) + ## plt.show() + + d = np.mean(np.abs(p0 - p1)) + assert d < 0.2 + +def test_swirl(): + image = img_as_float(data.checkerboard()) + + swirl_params = {'radius': 80, 'rotation': 0, 'order': 2, 'mode': 'reflect'} + swirled = tf.swirl(image, strength=10, **swirl_params) + unswirled = tf.swirl(swirled, strength=-10, **swirl_params) + + assert np.mean(np.abs(image - unswirled)) < 0.01 + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() diff --git a/skimage/transform/tests/test_project.py b/skimage/transform/tests/test_project.py deleted file mode 100644 index 25ebda20..00000000 --- a/skimage/transform/tests/test_project.py +++ /dev/null @@ -1,63 +0,0 @@ -import numpy as np -from numpy.testing import assert_array_almost_equal - -from skimage.transform._warp import _stackcopy -from skimage.transform import homography, fast_homography -from skimage import data -from skimage.color import rgb2gray - - -def test_stackcopy(): - layers = 4 - x = np.empty((3, 3, layers)) - y = np.eye(3, 3) - _stackcopy(x, y) - for i in range(layers): - assert_array_almost_equal(x[..., i], y) - - -def test_homography(): - x = np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1 - theta = -np.pi / 2 - M = np.array([[np.cos(theta), -np.sin(theta), 0], - [np.sin(theta), np.cos(theta), 2], - [0, 0, 1]]) - x90 = homography(x, M, order=1) - assert_array_almost_equal(x90, np.rot90(x)) - - -def test_fast_homography(): - img = rgb2gray(data.lena()).astype(np.uint8) - img = img[:, :100] - - theta = np.deg2rad(30) - scale = 0.5 - tx, ty = 50, 50 - - H = np.eye(3) - S = scale * np.sin(theta) - C = scale * np.cos(theta) - - H[:2, :2] = [[C, -S], [S, C]] - H[:2, 2] = [tx, ty] - - for mode in ('constant', 'mirror', 'wrap'): - p0 = homography(img, H, mode=mode, order=1) - p1 = fast_homography(img, H, mode=mode) - p1 = np.round(p1) - - ## import matplotlib.pyplot as plt - ## f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4) - ## ax0.imshow(img) - ## ax1.imshow(p0, cmap=plt.cm.gray) - ## ax2.imshow(p1, cmap=plt.cm.gray) - ## ax3.imshow(np.abs(p0 - p1), cmap=plt.cm.gray) - ## plt.show() - - d = np.mean(np.abs(p0 - p1)) - assert d < 0.2 - - -if __name__ == "__main__": - from numpy.testing import run_module_suite - run_module_suite() diff --git a/skimage/transform/tests/test_swirl.py b/skimage/transform/tests/test_swirl.py deleted file mode 100644 index d71f8231..00000000 --- a/skimage/transform/tests/test_swirl.py +++ /dev/null @@ -1,17 +0,0 @@ -import numpy as np -from numpy.testing import assert_array_almost_equal - -from skimage import transform as tf, data, img_as_float - - -def test_roundtrip(): - image = img_as_float(data.checkerboard()) - - swirl_params = {'radius': 80, 'rotation': 0, 'order': 2, 'mode': 'reflect'} - swirled = tf.swirl(image, strength=10, **swirl_params) - unswirled = tf.swirl(swirled, strength=-10, **swirl_params) - - assert np.mean(np.abs(image - unswirled)) < 0.01 - -if __name__ == "__main__": - np.testing.run_module_suite() From da3239d0ff879ad4823e3fcb468a34cf215551cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Mon, 4 Jun 2012 15:27:11 +0200 Subject: [PATCH 018/648] remove old import --- skimage/transform/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index f5621444..ad179cbb 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -1,7 +1,6 @@ from .hough_transform import * from .radon_transform import * from .finite_radon_transform import * -from .project import * from ._project import homography as fast_homography from .integral import * from .geometric import warp, make_tform, swirl, homography From bbd9280c2fc16cbef49afae436a174639a0770b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Mon, 4 Jun 2012 15:37:31 +0200 Subject: [PATCH 019/648] add example to make_tform doc string --- skimage/transform/geometric.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index 0306af59..7578b6fb 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -298,6 +298,16 @@ def make_tform(ttype, **kwargs): ------- tform : :class:`Transformation` tform object containing the transformation parameters + + Examples + -------- + >>> import numpy as np + >>> from skimage.transform import make_tform + >>> src = np.array([0, 0, 10, 10]).reshape((2, 2)) + >>> dst = np.array([12, 14, 1, -20]).reshape((2, 2)) + >>> tform = make_tform('similarity', src=src, dst=dst) + >>> print tform.matrix + >>> print tform.inv(tform.fwd(src)) # == src """ ttype = ttype.lower() From 33a7ddec0e986b28bec4cc3ce282f94221e7afa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Mon, 4 Jun 2012 18:14:38 +0200 Subject: [PATCH 020/648] update contributors --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index baaf064b..5961407d 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -107,3 +107,4 @@ Polygon, circle and ellipse drawing functions Adaptive thresholding Implementation of Matlab's `regionprops` + Estimation of geometric transformation parameters From fdf1b6dac1ceade98bf7df9fafff0c9ab493961f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Mon, 25 Jun 2012 19:57:02 +0200 Subject: [PATCH 021/648] fixe index errors and improve setup of some matrices --- skimage/transform/geometric.py | 89 +++++++++++++++------------------- 1 file changed, 38 insertions(+), 51 deletions(-) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index 7578b6fb..89e8cd75 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -35,8 +35,8 @@ def _make_similarity(src, dst): X = a0*x - b0*y + a1 Y = b0*x + a0*y + a2 where the homogeneous transformation matrix is: - [[a1 -b1 a0] - [b1 a1 b0] + [[a0 -b0 a1] + [b0 a0 b1] [0 0 1]] """ xs = src[:,0] @@ -45,32 +45,27 @@ def _make_similarity(src, dst): yd = dst[:,1] rows = src.shape[0] + #: params: a0, a1, b0, b1 A = np.zeros((rows*2, 4)) - b = np.zeros((rows*2,)) - A[:rows,0] = xs A[:rows,2] = - ys A[:rows,1] = 1 A[rows:,2] = xs A[rows:,0] = ys A[rows:,3] = 1 - b[:rows] = xd - b[rows:] = yd + + b = np.hstack([xd, yd]) a0, a1, b0, b1 = np.linalg.lstsq(A, b)[0] - matrix = np.eye(3) - matrix[0,0] = a0 - matrix[0,1] = - b0 - matrix[0,2] = a1 - matrix[1,0] = b0 - matrix[1,1] = a0 - matrix[1,2] = b1 + matrix = np.array([[a0, -b0, a1], + [b0, a0, b1], + [ 0, 0, 1]]) return matrix def _make_affine(src, dst): """Determine parameters of the 2D affine transformation: - X = a0*x + a1*y + a3 - Y = b0*x + b1*y + b3 + X = a0*x + a1*y + a2 + Y = b0*x + b1*y + b2 where the homogeneous transformation matrix is: [[a0 a1 a2] [b0 b1 b2] @@ -82,9 +77,8 @@ def _make_affine(src, dst): yd = dst[:,1] rows = src.shape[0] + #: params: a0, a1, a2, b0, b1, b2 A = np.zeros((rows*2, 6)) - b = np.zeros((rows*2,)) - A[:rows,0] = xs A[:rows,1] = ys A[:rows,2] = 1 @@ -92,22 +86,22 @@ def _make_affine(src, dst): A[rows:,4] = ys A[rows:,5] = 1 - b[:rows] = xd - b[rows:] = yd + b = np.hstack([xd, yd]) - params = np.linalg.lstsq(A, b)[0] - matrix = np.eye(3) - matrix[:2,:] = params.reshape((2, 3)) + a0, a1, a2, b0, b1, b2 = np.linalg.lstsq(A, b)[0] + matrix = np.array([[a0, a1, a2], + [b0, b1, b2], + [0, 0, 1]]) return matrix def _make_projective(src, dst): """Determine transformation matrix of the 2D projective transformation: - X = (a0 + a1*x + a2*y) / (c0*x + c1*y + c3) - Y = (b0 + b1*x + b2*y) / (c0*x + c1*y + c3) + X = (a0 + a1*x + a2*y) / (c0*x + c1*y + 1) + Y = (b0 + b1*x + b2*y) / (c0*x + c1*y + 1) where the homogeneous transformation matrix is: [[a0 a1 a2] [b0 b1 b2] - [c0 c1 c3]] + [c0 c1 1]] """ xs = src[:,0] ys = src[:,1] @@ -115,10 +109,8 @@ def _make_projective(src, dst): yd = dst[:,1] rows = src.shape[0] + #: params: a0, a1, a2, b0, b1, b2, c0, c1 A = np.zeros((rows*2, 8)) - b = np.zeros((rows*2,)) - - A[:rows,0] = xs A[:rows,1] = ys A[:rows,2] = 1 @@ -129,12 +121,14 @@ def _make_projective(src, dst): A[rows:,5] = 1 A[rows:,6] = - yd * xs A[rows:,7] = - yd * ys - b[:rows] = dst[:,0] - b[rows:] = dst[:,1] - matrix = np.eye(3).flatten() - matrix[:8] = np.linalg.lstsq(A, b)[0] - return matrix.reshape((3, 3)) + b = np.hstack([xd, yd]) + + a0, a1, a2, b0, b1, b2, c0, c1 = np.linalg.lstsq(A, b)[0] + matrix = np.array([[a0, a1, a2], + [b0, b1, b2], + [c0, c1, 1]]) + return matrix def _make_polynomial(src, dst, order): """Determine parameters of 2D polynomial transformation of order n: @@ -149,17 +143,16 @@ def _make_polynomial(src, dst, order): # number of unknown polynomial coefficients u = (order + 1) * (order + 2) - A = np.zeros((rows*2, u)) - b = np.zeros((rows*2,)) + A = np.zeros((rows*2, u)) pidx = 0 for j in xrange(order+1): for i in xrange(j+1): A[:rows,pidx] = xs ** (j - i) * ys ** i A[rows:,pidx+u/2] = xs ** (j - i) * ys ** i pidx += 1 - b[:rows] = xd - b[rows:] = yd + + b = np.hstack([xd, yd]) return np.linalg.lstsq(A, b)[0] @@ -171,8 +164,8 @@ def _make_rotation(angle): """ R = [ [math.cos(angle), -math.sin(angle), 0], - [math.sin(angle), math.cos(angle), 0], - [0, 0, 1], + [math.sin(angle), math.cos(angle), 0], + [0, 0, 1], ] return np.array(R) @@ -180,8 +173,9 @@ def _transform(coords, matrix): src = np.vstack((coords[:,0], coords[:,1], np.ones((coords.shape[0],)))) dst = np.dot(src.transpose(), matrix.transpose()) # rescale to homogeneous coordinates - dst[:,0] *= 1 / dst[:,2] - dst[:,1] *= 1 / dst[:,2] + dst[:,0] /= dst[:,2] + dst[:,1] /= dst[:,2] + # values close to zero because of limited numerical precision dst[np.abs(dst) < EPS] = 0 return dst[:,:2] @@ -334,10 +328,6 @@ def warp(image, reverse_map=None, map_args={}, tform=None, coordinates in the *source image*. Also see examples below. map_args : dict, optional Keyword arguments passed to `reverse_map`. - tform : :class:`Transformation` object - The inverse transformation will be used to transform coordinates in the - *output image* into their corresponding coordinates in the - *source image*. output_shape : tuple (rows, cols) Shape of the output image generated. order : int @@ -385,10 +375,7 @@ def warp(image, reverse_map=None, map_args={}, tform=None, # Map each (x, y) pair to the source image according to # the user-provided mapping - if callable(reverse_map): - tf_coords = reverse_map(tf_coords, **map_args) - else: - tf_coords = tform.inv(tf_coords) + tf_coords = reverse_map(tf_coords, **map_args) # Reshape back to a (2, M, N) coordinate grid tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2) @@ -564,5 +551,5 @@ def homography(image, H, output_shape=None, order=1, category=DeprecationWarning) tform = make_tform('projective', matrix=H) - return warp(image, tform=tform, output_shape=output_shape, order=order, - mode=mode, cval=cval) + return warp(image, reverse_map=tform.inv, output_shape=output_shape, + order=order, mode=mode, cval=cval) From acb1d71cd548728a0ae3e475361817001e7164fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Mon, 25 Jun 2012 20:03:59 +0200 Subject: [PATCH 022/648] apply PEP8 guideline --- skimage/transform/geometric.py | 136 ++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 62 deletions(-) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index 89e8cd75..7b59242e 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -30,6 +30,7 @@ def _stackcopy(a, b): else: a[:] = b + def _make_similarity(src, dst): """Determine parameters of the 2D similarity transformation: X = a0*x - b0*y + a1 @@ -39,20 +40,20 @@ def _make_similarity(src, dst): [b0 a0 b1] [0 0 1]] """ - xs = src[:,0] - ys = src[:,1] - xd = dst[:,0] - yd = dst[:,1] + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] rows = src.shape[0] #: params: a0, a1, b0, b1 - A = np.zeros((rows*2, 4)) - A[:rows,0] = xs - A[:rows,2] = - ys - A[:rows,1] = 1 - A[rows:,2] = xs - A[rows:,0] = ys - A[rows:,3] = 1 + A = np.zeros((rows * 2, 4)) + A[:rows, 0] = xs + A[:rows, 2] = - ys + A[:rows, 1] = 1 + A[rows:, 2] = xs + A[rows:, 0] = ys + A[rows:, 3] = 1 b = np.hstack([xd, yd]) @@ -62,6 +63,7 @@ def _make_similarity(src, dst): [ 0, 0, 1]]) return matrix + def _make_affine(src, dst): """Determine parameters of the 2D affine transformation: X = a0*x + a1*y + a2 @@ -71,20 +73,20 @@ def _make_affine(src, dst): [b0 b1 b2] [0 0 1]] """ - xs = src[:,0] - ys = src[:,1] - xd = dst[:,0] - yd = dst[:,1] + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] rows = src.shape[0] #: params: a0, a1, a2, b0, b1, b2 - A = np.zeros((rows*2, 6)) - A[:rows,0] = xs - A[:rows,1] = ys - A[:rows,2] = 1 - A[rows:,3] = xs - A[rows:,4] = ys - A[rows:,5] = 1 + A = np.zeros((rows * 2, 6)) + A[:rows, 0] = xs + A[:rows, 1] = ys + A[:rows, 2] = 1 + A[rows:, 3] = xs + A[rows:, 4] = ys + A[rows:, 5] = 1 b = np.hstack([xd, yd]) @@ -94,6 +96,7 @@ def _make_affine(src, dst): [0, 0, 1]]) return matrix + def _make_projective(src, dst): """Determine transformation matrix of the 2D projective transformation: X = (a0 + a1*x + a2*y) / (c0*x + c1*y + 1) @@ -103,24 +106,24 @@ def _make_projective(src, dst): [b0 b1 b2] [c0 c1 1]] """ - xs = src[:,0] - ys = src[:,1] - xd = dst[:,0] - yd = dst[:,1] + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] rows = src.shape[0] #: params: a0, a1, a2, b0, b1, b2, c0, c1 - A = np.zeros((rows*2, 8)) - A[:rows,0] = xs - A[:rows,1] = ys - A[:rows,2] = 1 - A[:rows,6] = - xd * xs - A[:rows,7] = - xd * ys - A[rows:,3] = xs - A[rows:,4] = ys - A[rows:,5] = 1 - A[rows:,6] = - yd * xs - A[rows:,7] = - yd * ys + A = np.zeros((rows * 2, 8)) + A[:rows, 0] = xs + A[:rows, 1] = ys + A[:rows, 2] = 1 + A[:rows, 6] = - xd * xs + A[:rows, 7] = - xd * ys + A[rows:, 3] = xs + A[rows:, 4] = ys + A[rows:, 5] = 1 + A[rows:, 6] = - yd * xs + A[rows:, 7] = - yd * ys b = np.hstack([xd, yd]) @@ -130,32 +133,34 @@ def _make_projective(src, dst): [c0, c1, 1]]) return matrix + def _make_polynomial(src, dst, order): """Determine parameters of 2D polynomial transformation of order n: X = sum[j=0:n]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) Y = sum[j=0:n]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) """ - xs = src[:,0] - ys = src[:,1] - xd = dst[:,0] - yd = dst[:,1] + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] rows = src.shape[0] # number of unknown polynomial coefficients u = (order + 1) * (order + 2) - A = np.zeros((rows*2, u)) + A = np.zeros((rows * 2, u)) pidx = 0 - for j in xrange(order+1): - for i in xrange(j+1): - A[:rows,pidx] = xs ** (j - i) * ys ** i - A[rows:,pidx+u/2] = xs ** (j - i) * ys ** i + for j in xrange(order + 1): + for i in xrange(j + 1): + A[:rows, pidx] = xs ** (j - i) * ys ** i + A[rows:, pidx + u / 2] = xs ** (j - i) * ys ** i pidx += 1 b = np.hstack([xd, yd]) return np.linalg.lstsq(A, b)[0] + def _make_rotation(angle): """Determine homogeneous transformation matrix of 2D rotation: [[cos(angle) -sin(angle) 0] @@ -169,29 +174,32 @@ def _make_rotation(angle): ] return np.array(R) + def _transform(coords, matrix): - src = np.vstack((coords[:,0], coords[:,1], np.ones((coords.shape[0],)))) + x, y = np.transpose(coords) + src = np.vstack((x, y, np.ones_like(x))) dst = np.dot(src.transpose(), matrix.transpose()) # rescale to homogeneous coordinates - dst[:,0] /= dst[:,2] - dst[:,1] /= dst[:,2] + dst[:, 0] /= dst[:, 2] + dst[:, 1] /= dst[:, 2] # values close to zero because of limited numerical precision dst[np.abs(dst) < EPS] = 0 - return dst[:,:2] + return dst[:, :2] + def _transform_polynomial(coords, matrix): - x = coords[:,0] - y = coords[:,1] + x = coords[:, 0] + y = coords[:, 1] u = len(matrix) # number of coefficients -> u = (order + 1) * (order + 2) - order = int((-3 + math.sqrt(9 - 4 * (2 - u))) / 2) + order = int((- 3 + math.sqrt(9 - 4 * (2 - u))) / 2) dst = np.zeros(coords.shape) pidx = 0 - for j in xrange(order+1): - for i in xrange(j+1): - dst[:,0] += matrix[pidx] * x ** (j - i) * y ** i - dst[:,1] += matrix[pidx+u/2] * x ** (j - i) * y ** i + for j in xrange(order + 1): + for i in xrange(j + 1): + dst[:, 0] += matrix[pidx] * x ** (j - i) * y ** i + dst[:, 1] += matrix[pidx + u / 2] * x ** (j - i) * y ** i pidx += 1 return dst @@ -283,8 +291,8 @@ def make_tform(ttype, **kwargs): 'polynomial' `src, `dst`, `order` (polynomial order) 'rotation' `angle` - Alternatively you can explicitly define a 3x3 homogeneous transformation - matrix with the `matrix` parameter. + Alternatively you can explicitly define a 3x3 homogeneous + transformation matrix with the `matrix` parameter. See examples section below for usage. @@ -314,8 +322,9 @@ def make_tform(ttype, **kwargs): matrix = TRANSFORMATIONS[ttype][0](**kwargs) return Transformation(ttype, matrix) -def warp(image, reverse_map=None, map_args={}, tform=None, - output_shape=None, order=1, mode='constant', cval=0.): + +def warp(image, reverse_map=None, map_args={}, output_shape=None, order=1, + mode='constant', cval=0.): """Warp an image according to a given coordinate transformation. Parameters @@ -398,10 +407,11 @@ def warp(image, reverse_map=None, map_args={}, tform=None, # so clip to ensure valid data return np.clip(mapped.squeeze(), 0, 1) + def _swirl_mapping(xy, center, rotation, strength, radius): x, y = xy.T x0, y0 = center - rho = np.sqrt((x - x0)**2 + (y - y0)**2) + rho = np.sqrt((x - x0) ** 2 + (y - y0) ** 2) # Ensure that the transformation decays to approximately 1/1000-th # within the specified radius. @@ -416,6 +426,7 @@ def _swirl_mapping(xy, center, rotation, strength, radius): return xy + def swirl(image, center=None, strength=1, radius=100, rotation=0, output_shape=None, order=1, mode='constant', cval=0): """Perform a swirl transformation. @@ -467,6 +478,7 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, output_shape=output_shape, order=order, mode=mode, cval=cval) + def homography(image, H, output_shape=None, order=1, mode='constant', cval=0.): """Perform a projective transformation (homography) on an image. From 8bde92b66cb2d296fd86b9af61a794fc8399b91b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Mon, 2 Jul 2012 09:27:35 +0200 Subject: [PATCH 023/648] remove inconsistent numeric correction and fix test case --- skimage/transform/geometric.py | 5 ----- skimage/transform/tests/test_geometric.py | 6 ++++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index 7b59242e..71ec7752 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -5,9 +5,6 @@ from scipy import ndimage from skimage.util import img_as_float -EPS = np.spacing(1) - - def _stackcopy(a, b): """Copy b into each color layer of a, such that:: @@ -182,8 +179,6 @@ def _transform(coords, matrix): # rescale to homogeneous coordinates dst[:, 0] /= dst[:, 2] dst[:, 1] /= dst[:, 2] - # values close to zero because of limited numerical precision - dst[np.abs(dst) < EPS] = 0 return dst[:, :2] diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index a227f597..d0edace1 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -92,10 +92,12 @@ def test_polynomial(): assert_array_almost_equal(tform.fwd(SRC), DST, 6) def test_homography(): - x = img_as_float(np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1) + x = np.zeros((5,5), dtype=np.uint8) + x[1, 1] = 255 + x = img_as_float(x) theta = -np.pi/2 M = np.array([[np.cos(theta),-np.sin(theta),0], - [np.sin(theta), np.cos(theta),2], + [np.sin(theta), np.cos(theta),4], [0, 0, 1]]) x90 = homography(x, M, order=1) assert_array_almost_equal(x90, np.rot90(x)) From cb3c93a1107b0b7f0308072693e18d48cfef9c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Mon, 9 Jul 2012 23:00:52 +0200 Subject: [PATCH 024/648] change API design and rename some functions --- skimage/transform/__init__.py | 3 +- skimage/transform/geometric.py | 145 ++++++++++++---------- skimage/transform/tests/test_geometric.py | 51 ++++---- 3 files changed, 108 insertions(+), 91 deletions(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index ad179cbb..296503d2 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -3,4 +3,5 @@ from .radon_transform import * from .finite_radon_transform import * from ._project import homography as fast_homography from .integral import * -from .geometric import warp, make_tform, swirl, homography +from .geometric import warp, estimate_transformation, geometric_transform, \ + swirl, homography diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index 71ec7752..abe27d4c 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -28,7 +28,7 @@ def _stackcopy(a, b): a[:] = b -def _make_similarity(src, dst): +def _estimate_similarity(src, dst): """Determine parameters of the 2D similarity transformation: X = a0*x - b0*y + a1 Y = b0*x + a0*y + a2 @@ -36,6 +36,7 @@ def _make_similarity(src, dst): [[a0 -b0 a1] [b0 a0 b1] [0 0 1]] + """ xs = src[:, 0] ys = src[:, 1] @@ -61,7 +62,7 @@ def _make_similarity(src, dst): return matrix -def _make_affine(src, dst): +def _estimate_affine(src, dst): """Determine parameters of the 2D affine transformation: X = a0*x + a1*y + a2 Y = b0*x + b1*y + b2 @@ -69,6 +70,7 @@ def _make_affine(src, dst): [[a0 a1 a2] [b0 b1 b2] [0 0 1]] + """ xs = src[:, 0] ys = src[:, 1] @@ -94,7 +96,7 @@ def _make_affine(src, dst): return matrix -def _make_projective(src, dst): +def _estimate_projective(src, dst): """Determine transformation matrix of the 2D projective transformation: X = (a0 + a1*x + a2*y) / (c0*x + c1*y + 1) Y = (b0 + b1*x + b2*y) / (c0*x + c1*y + 1) @@ -102,6 +104,7 @@ def _make_projective(src, dst): [[a0 a1 a2] [b0 b1 b2] [c0 c1 1]] + """ xs = src[:, 0] ys = src[:, 1] @@ -131,10 +134,11 @@ def _make_projective(src, dst): return matrix -def _make_polynomial(src, dst, order): +def _estimate_polynomial(src, dst, order): """Determine parameters of 2D polynomial transformation of order n: X = sum[j=0:n]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) Y = sum[j=0:n]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) + """ xs = src[:, 0] ys = src[:, 1] @@ -158,21 +162,21 @@ def _make_polynomial(src, dst, order): return np.linalg.lstsq(A, b)[0] -def _make_rotation(angle): - """Determine homogeneous transformation matrix of 2D rotation: - [[cos(angle) -sin(angle) 0] - [sin(angle) cos(angle) 0] - [0 0 1]] +def geometric_transform(coords, matrix): + """Apply 2D geometric transformation. + + Parameters + ---------- + ttype : Nx2 array + x, y coordinates to transform + matrix : 3x3 array + homogeneous transformation matrix + + Returns + ------- + coords : Nx2 array + transformed coordinates """ - R = [ - [math.cos(angle), -math.sin(angle), 0], - [math.sin(angle), math.cos(angle), 0], - [0, 0, 1], - ] - return np.array(R) - - -def _transform(coords, matrix): x, y = np.transpose(coords) src = np.vstack((x, y, np.ones_like(x))) dst = np.dot(src.transpose(), matrix.transpose()) @@ -200,32 +204,28 @@ def _transform_polynomial(coords, matrix): return dst -TRANSFORMATIONS = { - 'similarity': (_make_similarity, _transform), - 'affine': (_make_affine, _transform), - 'projective': (_make_projective, _transform), - 'polynomial': (_make_polynomial, _transform_polynomial), - 'rotation': (_make_rotation, _transform), -} +class GeometricTransformation(object): - -class Transformation(object): - - def __init__(self, ttype, matrix): - """Create transformation which contains the transformation parameters - and can perform forward and inverse transformations. + def __init__(self, ttype, params, transform_func): + """Create geometric transformation which contains the transformation + parameters and can perform forward and reverse transformations. Parameters ---------- ttype : str - one of similarity, affine, projective, polynomial, rotation - matrix : 3x3 array - homogeneous transformation matrix + transformation type - one of 'similarity', 'affine', 'projective', + 'polynomial' + params : array + transformation parameters + transform_func : callable = func(coords, matrix) + transformation function + """ self.ttype = ttype - self.matrix = matrix + self.params = params + self.transform_func = transform_func - def fwd(self, coords): + def forward(self, coords): """Apply forward transformation. Parameters @@ -237,11 +237,12 @@ class Transformation(object): ------- coords : Nx2 array transformed coordinates - """ - return TRANSFORMATIONS[self.ttype][1](coords, self.matrix) - def inv(self, coords): - """Apply inverse transformation. + """ + return self.transform_func(coords, self.params) + + def reverse(self, coords): + """Apply reverse transformation. Parameters ---------- @@ -252,30 +253,38 @@ class Transformation(object): ------- coords : Nx2 array transformed coordinates + """ if self.ttype == 'polynomial': raise Exception( - 'There is no explicit way to do the inverse polynomial ' - 'transformation. Instead determine the inverse transformation ' - 'parameters and use the forward transformation instead.') - matrix = np.linalg.inv(self.matrix) - return TRANSFORMATIONS[self.ttype][1](coords, matrix) + 'There is no explicit way to do the reverse polynomial ' + 'transformation. Instead determine the reverse transformation ' + 'parameters by exchanging source and destination coordinates.' + 'Then apply the forward transformation.') + inv_matrix = np.linalg.inv(self.params) + return self.transform_func(coords, inv_matrix) -def make_tform(ttype, **kwargs): - """Create geometric transformation. +ESTIMATED_TRANSFORMATIONS = { + 'similarity': (_estimate_similarity, geometric_transform), + 'affine': (_estimate_affine, geometric_transform), + 'projective': (_estimate_projective, geometric_transform), + 'polynomial': (_estimate_polynomial, _transform_polynomial), +} + + +def estimate_transformation(ttype, *args, **kwargs): + """Estimate 2D geometric transformation parameters. You can determine the over-, well- and under-determined parameters with the least-squares method. - - Number of source must match number of destination coordinates. Parameters ---------- ttype : str - one of similarity, affine, projective, polynomial, rotation + one of similarity, affine, projective, polynomial kwargs : array or int function parameters (src, dst, n, angle): @@ -284,17 +293,14 @@ def make_tform(ttype, **kwargs): 'affine' `src, `dst` 'projective' `src, `dst` 'polynomial' `src, `dst`, `order` (polynomial order) - 'rotation' `angle` - - Alternatively you can explicitly define a 3x3 homogeneous - transformation matrix with the `matrix` parameter. See examples section below for usage. Returns ------- - tform : :class:`Transformation` - tform object containing the transformation parameters + tform : :class:`GeometricTransformation` + tform object containing the transformation parameters and providing + access to forward and reverse transformation functions Examples -------- @@ -302,20 +308,23 @@ def make_tform(ttype, **kwargs): >>> from skimage.transform import make_tform >>> src = np.array([0, 0, 10, 10]).reshape((2, 2)) >>> dst = np.array([12, 14, 1, -20]).reshape((2, 2)) - >>> tform = make_tform('similarity', src=src, dst=dst) - >>> print tform.matrix - >>> print tform.inv(tform.fwd(src)) # == src - """ + >>> tform = estimate_transformation('similarity', src, dst) + >>> print tform.params + >>> print tform.reverse(tform.forward(src)) # == src + >>> # warp image using the transformation + >>> from skimage import data + >>> image = data.camera() + >>> warp(image, reverse_map=tform.forward) + >>> warp(image, reverse_map=tform.reverse) + """ ttype = ttype.lower() - if ttype not in TRANSFORMATIONS: + if ttype not in ESTIMATED_TRANSFORMATIONS: raise NotImplemented('the transformation type \'%s\' is not' 'implemented' % ttype) - if 'matrix' in kwargs: - matrix = kwargs['matrix'] - else: - matrix = TRANSFORMATIONS[ttype][0](**kwargs) - return Transformation(ttype, matrix) + matrix = ESTIMATED_TRANSFORMATIONS[ttype][0](*args, **kwargs) + transform_func = ESTIMATED_TRANSFORMATIONS[ttype][1] + return GeometricTransformation(ttype, matrix, transform_func) def warp(image, reverse_map=None, map_args={}, output_shape=None, order=1, @@ -557,6 +566,6 @@ def homography(image, H, output_shape=None, order=1, 'use the `warp` and `tform` function instead', category=DeprecationWarning) - tform = make_tform('projective', matrix=H) - return warp(image, reverse_map=tform.inv, output_shape=output_shape, + tform = GeometricTransformation('projective', H, geometric_transform) + return warp(image, reverse_map=tform.reverse, output_shape=output_shape, order=order, mode=mode, cval=cval) diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index d0edace1..247637bb 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -2,7 +2,7 @@ import numpy as np from numpy.testing import assert_array_almost_equal from skimage.transform.geometric import _stackcopy -from skimage.transform import make_tform +from skimage.transform import estimate_transformation from skimage.transform import homography, fast_homography from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray @@ -36,60 +36,65 @@ def test_stackcopy(): y = np.eye(3, 3) _stackcopy(x, y) for i in range(layers): - assert_array_almost_equal(x[...,i], y) + assert_array_almost_equal(x[..., i], y) + def test_similarity(): #: exact solution - tform = make_tform('similarity', src=SRC[:2,:], dst=DST[:2,:]) - assert_array_almost_equal(tform.fwd(SRC[:2,:]), DST[:2,:]) - assert_array_almost_equal(tform.inv(tform.fwd(SRC)), SRC) + tform = estimate_transformation('similarity', SRC[:2, :], DST[:2, :]) + assert_array_almost_equal(tform.forward(SRC[:2, :]), DST[:2, :]) + assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) #: over-determined - tform = make_tform('similarity', src=SRC, dst=DST) + tform = estimate_transformation('similarity', SRC, DST) ref = np.array( [[2.3632898110e+02, -5.5876792257e+00, 2.5331569391e+03], [5.5876792257e+00, 2.3632898110e+02, 2.4358232635e+03], [0.0000000000e+00, 0.0000000000e+00, 1.0000000000e+00]]) - assert_array_almost_equal(tform.matrix, ref) - assert_array_almost_equal(tform.inv(tform.fwd(SRC)), SRC) + assert_array_almost_equal(tform.params, ref) + assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) + def test_affine(): #: exact solution - tform = make_tform('affine', src=SRC[:3,:], dst=DST[:3,:]) - assert_array_almost_equal(tform.fwd(SRC[:3,:]), DST[:3,:]) - assert_array_almost_equal(tform.inv(tform.fwd(SRC)), SRC) + tform = estimate_transformation('affine', SRC[:3, :], DST[:3, :]) + assert_array_almost_equal(tform.forward(SRC[:3, :]), DST[:3, :]) + assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) #: over-determined - tform = make_tform('affine', src=SRC, dst=DST) + tform = estimate_transformation('affine', SRC, DST) ref = np.array( [[2.2573930047e+02, 7.1588596765e+00, 2.5126622012e+03], [2.1234856855e+01, 2.4931019555e+02, 2.4143862183e+03], [0.0000000000e+00, 0.0000000000e+00, 1.0000000000e+00]]) - assert_array_almost_equal(tform.matrix, ref) - assert_array_almost_equal(tform.inv(tform.fwd(SRC)), SRC) + assert_array_almost_equal(tform.params, ref) + assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) + def test_projective(): #: exact solution - tform = make_tform('projective', src=SRC[:4,:], dst=DST[:4,:]) + tform = estimate_transformation('projective', SRC[:4, :], DST[:4, :]) ref = np.array( [[ 1.9466901291e+02, -1.1888183994e+01, 2.2832379309e+03], [ -8.6910077540e+00, 2.2162069773e+02, 2.2211673699e+03], [ -1.2695966735e-02, -9.6053624285e-03, 1.0000000000e+00]]) - assert_array_almost_equal(tform.matrix, ref, 6) - assert_array_almost_equal(tform.inv(tform.fwd(SRC)), SRC) + assert_array_almost_equal(tform.params, ref, 6) + assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) #: over-determined - tform = make_tform('projective', src=SRC[:4,:], dst=DST[:4,:]) + tform = estimate_transformation('projective', SRC[:4, :], DST[:4, :]) ref = np.array( [[ 1.9466901291e+02, -1.1888183994e+01, 2.2832379309e+03], [ -8.6910077540e+00, 2.2162069773e+02, 2.2211673699e+03], [ -1.2695966735e-02, -9.6053624285e-03, 1.0000000000e+00]]) - assert_array_almost_equal(tform.matrix, ref, 6) - assert_array_almost_equal(tform.inv(tform.fwd(SRC)), SRC) + assert_array_almost_equal(tform.params, ref, 6) + assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) + def test_polynomial(): - tform = make_tform('polynomial', src=SRC, dst=DST, order=10) - assert_array_almost_equal(tform.fwd(SRC), DST, 6) + tform = estimate_transformation('polynomial', SRC, DST, order=10) + assert_array_almost_equal(tform.forward(SRC), DST, 6) + def test_homography(): x = np.zeros((5,5), dtype=np.uint8) @@ -102,6 +107,7 @@ def test_homography(): x90 = homography(x, M, order=1) assert_array_almost_equal(x90, np.rot90(x)) + def test_fast_homography(): img = rgb2gray(data.lena()).astype(np.uint8) img = img[:, :100] @@ -133,6 +139,7 @@ def test_fast_homography(): d = np.mean(np.abs(p0 - p1)) assert d < 0.2 + def test_swirl(): image = img_as_float(data.checkerboard()) From 2a75b78838112d214a3ac44ddde49239a099d05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Tue, 10 Jul 2012 19:36:32 +0200 Subject: [PATCH 025/648] change arguments of function estimate_transformation --- skimage/transform/geometric.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index abe27d4c..68f2dae8 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -273,7 +273,7 @@ ESTIMATED_TRANSFORMATIONS = { } -def estimate_transformation(ttype, *args, **kwargs): +def estimate_transformation(ttype, src, dst, order=None): """Estimate 2D geometric transformation parameters. You can determine the over-, well- and under-determined parameters @@ -322,7 +322,10 @@ def estimate_transformation(ttype, *args, **kwargs): if ttype not in ESTIMATED_TRANSFORMATIONS: raise NotImplemented('the transformation type \'%s\' is not' 'implemented' % ttype) - matrix = ESTIMATED_TRANSFORMATIONS[ttype][0](*args, **kwargs) + args = [src, dst] + if order is not None and ttype == 'polynomial': + args.append(order) + matrix = ESTIMATED_TRANSFORMATIONS[ttype][0](*args) transform_func = ESTIMATED_TRANSFORMATIONS[ttype][1] return GeometricTransformation(ttype, matrix, transform_func) From ec5c339c682fb2187d77d2c35ce12cf77a95bb6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Tue, 10 Jul 2012 19:37:33 +0200 Subject: [PATCH 026/648] fix wrong exception type --- skimage/transform/geometric.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index 68f2dae8..4519af88 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -320,8 +320,8 @@ def estimate_transformation(ttype, src, dst, order=None): """ ttype = ttype.lower() if ttype not in ESTIMATED_TRANSFORMATIONS: - raise NotImplemented('the transformation type \'%s\' is not' - 'implemented' % ttype) + raise ValueError('the transformation type \'%s\' is not' + 'implemented' % ttype) args = [src, dst] if order is not None and ttype == 'polynomial': args.append(order) From 640edc2a6261e54cf4a0108b3120ceffdf339d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Tue, 10 Jul 2012 19:38:19 +0200 Subject: [PATCH 027/648] fix doc string formatting of function estimate_transformation --- skimage/transform/geometric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index 4519af88..ec631043 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -285,7 +285,7 @@ def estimate_transformation(ttype, src, dst, order=None): ---------- ttype : str one of similarity, affine, projective, polynomial - kwargs : array or int + kwargs :: array or int function parameters (src, dst, n, angle): NAME / TTYPE FUNCTION PARAMETERS From e2ce1d63de70243ad3f5da0959cbde90db40e780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Tue, 10 Jul 2012 22:58:10 +0200 Subject: [PATCH 028/648] redesign class interface --- skimage/transform/__init__.py | 3 +- skimage/transform/geometric.py | 523 ++++++++++++++-------- skimage/transform/tests/test_geometric.py | 40 +- 3 files changed, 374 insertions(+), 192 deletions(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 296503d2..b0790992 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -4,4 +4,5 @@ from .finite_radon_transform import * from ._project import homography as fast_homography from .integral import * from .geometric import warp, estimate_transformation, geometric_transform, \ - swirl, homography + SimilarityTransformation, AffineTransformation, ProjectiveTransformation, \ + PolynomialTransformation, swirl, homography diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index ec631043..d5f50b17 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -28,140 +28,6 @@ def _stackcopy(a, b): a[:] = b -def _estimate_similarity(src, dst): - """Determine parameters of the 2D similarity transformation: - X = a0*x - b0*y + a1 - Y = b0*x + a0*y + a2 - where the homogeneous transformation matrix is: - [[a0 -b0 a1] - [b0 a0 b1] - [0 0 1]] - - """ - xs = src[:, 0] - ys = src[:, 1] - xd = dst[:, 0] - yd = dst[:, 1] - rows = src.shape[0] - - #: params: a0, a1, b0, b1 - A = np.zeros((rows * 2, 4)) - A[:rows, 0] = xs - A[:rows, 2] = - ys - A[:rows, 1] = 1 - A[rows:, 2] = xs - A[rows:, 0] = ys - A[rows:, 3] = 1 - - b = np.hstack([xd, yd]) - - a0, a1, b0, b1 = np.linalg.lstsq(A, b)[0] - matrix = np.array([[a0, -b0, a1], - [b0, a0, b1], - [ 0, 0, 1]]) - return matrix - - -def _estimate_affine(src, dst): - """Determine parameters of the 2D affine transformation: - X = a0*x + a1*y + a2 - Y = b0*x + b1*y + b2 - where the homogeneous transformation matrix is: - [[a0 a1 a2] - [b0 b1 b2] - [0 0 1]] - - """ - xs = src[:, 0] - ys = src[:, 1] - xd = dst[:, 0] - yd = dst[:, 1] - rows = src.shape[0] - - #: params: a0, a1, a2, b0, b1, b2 - A = np.zeros((rows * 2, 6)) - A[:rows, 0] = xs - A[:rows, 1] = ys - A[:rows, 2] = 1 - A[rows:, 3] = xs - A[rows:, 4] = ys - A[rows:, 5] = 1 - - b = np.hstack([xd, yd]) - - a0, a1, a2, b0, b1, b2 = np.linalg.lstsq(A, b)[0] - matrix = np.array([[a0, a1, a2], - [b0, b1, b2], - [0, 0, 1]]) - return matrix - - -def _estimate_projective(src, dst): - """Determine transformation matrix of the 2D projective transformation: - X = (a0 + a1*x + a2*y) / (c0*x + c1*y + 1) - Y = (b0 + b1*x + b2*y) / (c0*x + c1*y + 1) - where the homogeneous transformation matrix is: - [[a0 a1 a2] - [b0 b1 b2] - [c0 c1 1]] - - """ - xs = src[:, 0] - ys = src[:, 1] - xd = dst[:, 0] - yd = dst[:, 1] - rows = src.shape[0] - - #: params: a0, a1, a2, b0, b1, b2, c0, c1 - A = np.zeros((rows * 2, 8)) - A[:rows, 0] = xs - A[:rows, 1] = ys - A[:rows, 2] = 1 - A[:rows, 6] = - xd * xs - A[:rows, 7] = - xd * ys - A[rows:, 3] = xs - A[rows:, 4] = ys - A[rows:, 5] = 1 - A[rows:, 6] = - yd * xs - A[rows:, 7] = - yd * ys - - b = np.hstack([xd, yd]) - - a0, a1, a2, b0, b1, b2, c0, c1 = np.linalg.lstsq(A, b)[0] - matrix = np.array([[a0, a1, a2], - [b0, b1, b2], - [c0, c1, 1]]) - return matrix - - -def _estimate_polynomial(src, dst, order): - """Determine parameters of 2D polynomial transformation of order n: - X = sum[j=0:n]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) - Y = sum[j=0:n]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) - - """ - xs = src[:, 0] - ys = src[:, 1] - xd = dst[:, 0] - yd = dst[:, 1] - rows = src.shape[0] - - # number of unknown polynomial coefficients - u = (order + 1) * (order + 2) - - A = np.zeros((rows * 2, u)) - pidx = 0 - for j in xrange(order + 1): - for i in xrange(j + 1): - A[:rows, pidx] = xs ** (j - i) * ys ** i - A[rows:, pidx + u / 2] = xs ** (j - i) * ys ** i - pidx += 1 - - b = np.hstack([xd, yd]) - - return np.linalg.lstsq(A, b)[0] - - def geometric_transform(coords, matrix): """Apply 2D geometric transformation. @@ -186,44 +52,20 @@ def geometric_transform(coords, matrix): return dst[:, :2] -def _transform_polynomial(coords, matrix): - x = coords[:, 0] - y = coords[:, 1] - u = len(matrix) - # number of coefficients -> u = (order + 1) * (order + 2) - order = int((- 3 + math.sqrt(9 - 4 * (2 - u))) / 2) - dst = np.zeros(coords.shape) - - pidx = 0 - for j in xrange(order + 1): - for i in xrange(j + 1): - dst[:, 0] += matrix[pidx] * x ** (j - i) * y ** i - dst[:, 1] += matrix[pidx + u / 2] * x ** (j - i) * y ** i - pidx += 1 - - return dst - - class GeometricTransformation(object): - def __init__(self, ttype, params, transform_func): + def __init__(self, matrix=None): """Create geometric transformation which contains the transformation parameters and can perform forward and reverse transformations. Parameters ---------- - ttype : str - transformation type - one of 'similarity', 'affine', 'projective', - 'polynomial' - params : array - transformation parameters - transform_func : callable = func(coords, matrix) - transformation function + matrix : 3x3 array, optional + homogeneous transformation matrix """ - self.ttype = ttype - self.params = params - self.transform_func = transform_func + self.matrix = matrix + self.inverse_matrix = None def forward(self, coords): """Apply forward transformation. @@ -239,7 +81,9 @@ class GeometricTransformation(object): transformed coordinates """ - return self.transform_func(coords, self.params) + if self.matrix is None: + raise Exception('Transformation matrix must be set or estimated.') + return geometric_transform(coords, self.matrix) def reverse(self, coords): """Apply reverse transformation. @@ -255,21 +99,332 @@ class GeometricTransformation(object): transformed coordinates """ - if self.ttype == 'polynomial': - raise Exception( - 'There is no explicit way to do the reverse polynomial ' - 'transformation. Instead determine the reverse transformation ' - 'parameters by exchanging source and destination coordinates.' - 'Then apply the forward transformation.') - inv_matrix = np.linalg.inv(self.params) - return self.transform_func(coords, inv_matrix) + if self.matrix is None: + raise Exception('Transformation matrix must be set or estimated.') + if self.inverse_matrix is None: + self.inverse_matrix = np.linalg.inv(self.matrix) + return geometric_transform(coords, self.inverse_matrix) + + def union(self, other): + return GeometricTransformation(self.matrix.dot(other.matrix)) + + def __mul__(self, other): + return self.union(self, other) + + def __add__(self, other): + return self.union(self, other) -ESTIMATED_TRANSFORMATIONS = { - 'similarity': (_estimate_similarity, geometric_transform), - 'affine': (_estimate_affine, geometric_transform), - 'projective': (_estimate_projective, geometric_transform), - 'polynomial': (_estimate_polynomial, _transform_polynomial), +class SimilarityTransformation(GeometricTransformation): + + """2D similarity transformation of the following form: + X = a0*x - b0*y + a1 = + = m*x*cos(rotation) - m*y*sin(rotation) + a1 + Y = b0*x + a0*y + b1 = + = m*x*sin(rotation) + m*y*cos(rotation) + b1 + where the homogeneous transformation matrix is: + [[a0 -b0 a1] + [b0 a0 b1] + [0 0 1]] + + """ + + def estimate(self, src, dst): + """Set the transformation matrix with the estimated parameters of the + given control points. + + Parameters + ---------- + src : Nx2 array + source coordinates + dst : Nx2 array + destination coordinates + + """ + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] + rows = src.shape[0] + + #: params: a0, a1, b0, b1 + A = np.zeros((rows * 2, 4)) + A[:rows, 0] = xs + A[:rows, 2] = - ys + A[:rows, 1] = 1 + A[rows:, 2] = xs + A[rows:, 0] = ys + A[rows:, 3] = 1 + + b = np.hstack([xd, yd]) + + a0, a1, b0, b1 = np.linalg.lstsq(A, b)[0] + self.matrix = np.array([[a0, -b0, a1], + [b0, a0, b1], + [ 0, 0, 1]]) + + def from_params(self, scale, rotation, translation): + """Set the transformation matrix with the explicit transformation + parameters. + + Parameters + ---------- + scale : float + scale factor + rotation : float + rotation angle in counter-clockwise direction + translation : (tx, ty) as array, list or tuple + x, y translation parameters + + """ + self.matrix = np.array([ + [math.cos(rotation), - math.sin(rotation), 0], + [math.sin(rotation), math.cos(rotation), 0], + [ 0, 0, 1] + ]) + self.matrix *= scale + self.matrix[0:2, 2] = translation + + @property + def scale(self): + return self.matrix[0, 0] / math.cos(self.rotation) + + @property + def rotation(self): + return math.atan2(self.matrix[1, 0], self.matrix[1, 1]) + + @property + def translation(self): + return self.matrix[0:2, 2] + + +class AffineTransformation(GeometricTransformation): + + """2D affine transformation of the following form + X = a0*x + a1*y + a2 = + = sx*x*cos(rotation) - sy*y*sin(rotation+shear) + a2 + Y = b0*x + b1*y + b2 = + = sx*x*sin(rotation) + sy*y*cos(rotation+shear) + b2 + where the homogeneous transformation matrix is: + [[a0 a1 a2] + [b0 b1 b2] + [0 0 1]] + + """ + + def estimate(self, src, dst): + """Set the transformation matrix with the estimated parameters of the + given control points. + + Parameters + ---------- + src : Nx2 array + source coordinates + dst : Nx2 array + destination coordinates + + """ + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] + rows = src.shape[0] + + #: params: a0, a1, a2, b0, b1, b2 + A = np.zeros((rows * 2, 6)) + A[:rows, 0] = xs + A[:rows, 1] = ys + A[:rows, 2] = 1 + A[rows:, 3] = xs + A[rows:, 4] = ys + A[rows:, 5] = 1 + + b = np.hstack([xd, yd]) + + a0, a1, a2, b0, b1, b2 = np.linalg.lstsq(A, b)[0] + self.matrix = np.array([[a0, a1, a2], + [b0, b1, b2], + [0, 0, 1]]) + + def from_params(self, scale, rotation, shear, translation): + """Set the transformation matrix with the explicit transformation + parameters. + + Parameters + ---------- + scale : (sx, sy) as array, list or tuple + scale factors + rotation : float + rotation angle in counter-clockwise direction + shear : float + shear angle in counter-clockwise direction + translation : (tx, ty) as array, list or tuple + translation parameters + + """ + sx, sy = scale + self.matrix = 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 + + @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) + return sx, sy + + @property + def rotation(self): + return math.atan2(self.matrix[1, 0], self.matrix[0, 0]) + + @property + def shear(self): + beta = math.atan2(- self.matrix[0, 1], self.matrix[1, 1]) + return beta - self.rotation + + @property + def translation(self): + return self.matrix[0:2, 2] + + +class ProjectiveTransformation(GeometricTransformation): + + def estimate(self, src, dst): + """Estimate transformation matrix of the 2D projective transformation: + X = (a0 + a1*x + a2*y) / (c0*x + c1*y + 1) + Y = (b0 + b1*x + b2*y) / (c0*x + c1*y + 1) + where the homogeneous transformation matrix is: + [[a0 a1 a2] + [b0 b1 b2] + [c0 c1 1]] + + Parameters + ---------- + src : Nx2 array + source coordinates + dst : Nx2 array + destination coordinates + + """ + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] + rows = src.shape[0] + + #: params: a0, a1, a2, b0, b1, b2, c0, c1 + A = np.zeros((rows * 2, 8)) + A[:rows, 0] = xs + A[:rows, 1] = ys + A[:rows, 2] = 1 + A[:rows, 6] = - xd * xs + A[:rows, 7] = - xd * ys + A[rows:, 3] = xs + A[rows:, 4] = ys + A[rows:, 5] = 1 + A[rows:, 6] = - yd * xs + A[rows:, 7] = - yd * ys + + b = np.hstack([xd, yd]) + + a0, a1, a2, b0, b1, b2, c0, c1 = np.linalg.lstsq(A, b)[0] + self.matrix = np.array([[a0, a1, a2], + [b0, b1, b2], + [c0, c1, 1]]) + + +class PolynomialTransformation(GeometricTransformation): + + def __init__(self, coeffs=None): + """Create polynomial transformation which contains the transformation + parameters and can perform forward and reverse transformations. + + Parameters + ---------- + coeffs : array, optional + polynomial coefficients + + """ + self.coeffs = coeffs + + def estimate(self, src, dst, order): + """Estimate parameters of 2D polynomial transformation of order n: + X = sum[j=0:n]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) + Y = sum[j=0:n]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) + + Parameters + ---------- + src : Nx2 array + source coordinates + dst : Nx2 array + destination coordinates + order : int + polynomial order (number of coefficients is order + 1) + + """ + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] + rows = src.shape[0] + + # number of unknown polynomial coefficients + u = (order + 1) * (order + 2) + + A = np.zeros((rows * 2, u)) + pidx = 0 + for j in xrange(order + 1): + for i in xrange(j + 1): + A[:rows, pidx] = xs ** (j - i) * ys ** i + A[rows:, pidx + u / 2] = xs ** (j - i) * ys ** i + pidx += 1 + + b = np.hstack([xd, yd]) + + self.coeffs = np.linalg.lstsq(A, b)[0] + + def forward(self, coords): + x = coords[:, 0] + y = coords[:, 1] + u = len(self.coeffs) + # number of coefficients -> u = (order + 1) * (order + 2) + order = int((- 3 + math.sqrt(9 - 4 * (2 - u))) / 2) + dst = np.zeros(coords.shape) + + pidx = 0 + for j in xrange(order + 1): + for i in xrange(j + 1): + dst[:, 0] += self.coeffs[pidx] * x ** (j - i) * y ** i + dst[:, 1] += self.coeffs[pidx + u / 2] * x ** (j - i) * y ** i + pidx += 1 + + return dst + + def reverse(self, coords): + raise Exception( + 'There is no explicit way to do the reverse polynomial ' + 'transformation. Instead determine the reverse transformation ' + 'parameters by exchanging source and destination coordinates.' + 'Then apply the forward transformation.') + + def union(self, other): + raise Exception('Cannot unite polynomial transformations.') + + def __mul__(self, other): + return self.union(self, other) + + def __add__(self, other): + return self.union(self, other) + + +TRANSFORMATIONS = { + 'similarity': SimilarityTransformation, + 'affine': AffineTransformation, + 'projective': ProjectiveTransformation, + 'polynomial': PolynomialTransformation, } @@ -298,7 +453,7 @@ def estimate_transformation(ttype, src, dst, order=None): Returns ------- - tform : :class:`GeometricTransformation` + tform : subclass of :class:`GeometricTransformation` tform object containing the transformation parameters and providing access to forward and reverse transformation functions @@ -309,7 +464,7 @@ def estimate_transformation(ttype, src, dst, order=None): >>> src = np.array([0, 0, 10, 10]).reshape((2, 2)) >>> dst = np.array([12, 14, 1, -20]).reshape((2, 2)) >>> tform = estimate_transformation('similarity', src, dst) - >>> print tform.params + >>> print tform.matrix >>> print tform.reverse(tform.forward(src)) # == src >>> # warp image using the transformation >>> from skimage import data @@ -319,15 +474,15 @@ def estimate_transformation(ttype, src, dst, order=None): """ ttype = ttype.lower() - if ttype not in ESTIMATED_TRANSFORMATIONS: + if ttype not in TRANSFORMATIONS: raise ValueError('the transformation type \'%s\' is not' 'implemented' % ttype) args = [src, dst] if order is not None and ttype == 'polynomial': args.append(order) - matrix = ESTIMATED_TRANSFORMATIONS[ttype][0](*args) - transform_func = ESTIMATED_TRANSFORMATIONS[ttype][1] - return GeometricTransformation(ttype, matrix, transform_func) + tform = TRANSFORMATIONS[ttype]() + tform.estimate(*args) + return tform def warp(image, reverse_map=None, map_args={}, output_shape=None, order=1, @@ -569,6 +724,6 @@ def homography(image, H, output_shape=None, order=1, 'use the `warp` and `tform` function instead', category=DeprecationWarning) - tform = GeometricTransformation('projective', H, geometric_transform) + tform = ProjectiveTransformation(H) return warp(image, reverse_map=tform.reverse, output_shape=output_shape, order=order, mode=mode, cval=cval) diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index 247637bb..0285a53d 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -2,7 +2,9 @@ import numpy as np from numpy.testing import assert_array_almost_equal from skimage.transform.geometric import _stackcopy -from skimage.transform import estimate_transformation +from skimage.transform import estimate_transformation, \ + SimilarityTransformation, AffineTransformation, ProjectiveTransformation, \ + PolynomialTransformation from skimage.transform import homography, fast_homography from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray @@ -39,7 +41,7 @@ def test_stackcopy(): assert_array_almost_equal(x[..., i], y) -def test_similarity(): +def test_similarity_estimation(): #: exact solution tform = estimate_transformation('similarity', SRC[:2, :], DST[:2, :]) assert_array_almost_equal(tform.forward(SRC[:2, :]), DST[:2, :]) @@ -51,11 +53,22 @@ def test_similarity(): [[2.3632898110e+02, -5.5876792257e+00, 2.5331569391e+03], [5.5876792257e+00, 2.3632898110e+02, 2.4358232635e+03], [0.0000000000e+00, 0.0000000000e+00, 1.0000000000e+00]]) - assert_array_almost_equal(tform.params, ref) + assert_array_almost_equal(tform.matrix, ref) assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) -def test_affine(): +def test_similarity_explicit(): + tform = SimilarityTransformation() + scale = 0.1 + rotation = 1 + translation = (1, 1) + tform.from_params(scale, rotation, 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 tform = estimate_transformation('affine', SRC[:3, :], DST[:3, :]) assert_array_almost_equal(tform.forward(SRC[:3, :]), DST[:3, :]) @@ -67,10 +80,23 @@ def test_affine(): [[2.2573930047e+02, 7.1588596765e+00, 2.5126622012e+03], [2.1234856855e+01, 2.4931019555e+02, 2.4143862183e+03], [0.0000000000e+00, 0.0000000000e+00, 1.0000000000e+00]]) - assert_array_almost_equal(tform.params, ref) + assert_array_almost_equal(tform.matrix, ref) assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) +def test_affine_explicit(): + tform = AffineTransformation() + scale = (0.1, 0.13) + rotation = 1 + shear = 0.1 + translation = (1, 1) + tform.from_params(scale, rotation, shear, translation) + assert_array_almost_equal(tform.scale, scale) + assert_array_almost_equal(tform.rotation, rotation) + assert_array_almost_equal(tform.shear, shear) + assert_array_almost_equal(tform.translation, translation) + + def test_projective(): #: exact solution tform = estimate_transformation('projective', SRC[:4, :], DST[:4, :]) @@ -78,7 +104,7 @@ def test_projective(): [[ 1.9466901291e+02, -1.1888183994e+01, 2.2832379309e+03], [ -8.6910077540e+00, 2.2162069773e+02, 2.2211673699e+03], [ -1.2695966735e-02, -9.6053624285e-03, 1.0000000000e+00]]) - assert_array_almost_equal(tform.params, ref, 6) + assert_array_almost_equal(tform.matrix, ref, 6) assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) #: over-determined @@ -87,7 +113,7 @@ def test_projective(): [[ 1.9466901291e+02, -1.1888183994e+01, 2.2832379309e+03], [ -8.6910077540e+00, 2.2162069773e+02, 2.2211673699e+03], [ -1.2695966735e-02, -9.6053624285e-03, 1.0000000000e+00]]) - assert_array_almost_equal(tform.params, ref, 6) + assert_array_almost_equal(tform.matrix, ref, 6) assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) From 234810be1070f3f3012cb14a192dae16c45e5bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Tue, 10 Jul 2012 23:01:07 +0200 Subject: [PATCH 029/648] fix inconsistent doc strings --- skimage/transform/geometric.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index d5f50b17..03e088a2 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -292,14 +292,19 @@ class AffineTransformation(GeometricTransformation): class ProjectiveTransformation(GeometricTransformation): + """2D projective transformation of the following form + X = (a0 + a1*x + a2*y) / (c0*x + c1*y + 1) + Y = (b0 + b1*x + b2*y) / (c0*x + c1*y + 1) + where the homogeneous transformation matrix is: + [[a0 a1 a2] + [b0 b1 b2] + [c0 c1 1]] + + """ + def estimate(self, src, dst): - """Estimate transformation matrix of the 2D projective transformation: - X = (a0 + a1*x + a2*y) / (c0*x + c1*y + 1) - Y = (b0 + b1*x + b2*y) / (c0*x + c1*y + 1) - where the homogeneous transformation matrix is: - [[a0 a1 a2] - [b0 b1 b2] - [c0 c1 1]] + """Set the transformation matrix with the explicit transformation + parameters. Parameters ---------- @@ -338,6 +343,12 @@ class ProjectiveTransformation(GeometricTransformation): class PolynomialTransformation(GeometricTransformation): + """2D affine transformation of the following form + X = sum[j=0:n]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) + Y = sum[j=0:n]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) + + """ + def __init__(self, coeffs=None): """Create polynomial transformation which contains the transformation parameters and can perform forward and reverse transformations. @@ -351,9 +362,8 @@ class PolynomialTransformation(GeometricTransformation): self.coeffs = coeffs def estimate(self, src, dst, order): - """Estimate parameters of 2D polynomial transformation of order n: - X = sum[j=0:n]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) - Y = sum[j=0:n]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) + """Set the transformation matrix with the explicit transformation + parameters. Parameters ---------- From b2ca8335094fb5787cc3a56d00239376dc5db38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Tue, 10 Jul 2012 23:20:32 +0200 Subject: [PATCH 030/648] fix transformation union and add test case --- skimage/transform/geometric.py | 10 +++++++--- skimage/transform/tests/test_geometric.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index 03e088a2..06b0839e 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -106,13 +106,17 @@ class GeometricTransformation(object): return geometric_transform(coords, self.inverse_matrix) def union(self, other): - return GeometricTransformation(self.matrix.dot(other.matrix)) + if type(self) == type(other): + transformation = self.__class__ + else: + transformation = GeometricTransformation + return transformation(self.matrix.dot(other.matrix)) def __mul__(self, other): - return self.union(self, other) + return self.union(other) def __add__(self, other): - return self.union(self, other) + return self.union(other) class SimilarityTransformation(GeometricTransformation): diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index 0285a53d..14e839f0 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -122,6 +122,27 @@ def test_polynomial(): assert_array_almost_equal(tform.forward(SRC), DST, 6) +def test_union(): + tform1 = SimilarityTransformation() + scale1 = 0.1 + rotation1 = 1 + translation1 = (0, 0) + tform1.from_params(scale1, rotation1, translation1) + + tform2 = SimilarityTransformation() + scale2 = 0.1 + rotation2 = 1 + translation2 = (0, 0) + tform2.from_params(scale2, rotation2, translation2) + + tform = tform1.union(tform2) + tform = tform1 + tform2 + tform = tform1 * tform2 + + assert_array_almost_equal(tform.scale, scale1 * scale2) + assert_array_almost_equal(tform.rotation, rotation1 + rotation2) + + def test_homography(): x = np.zeros((5,5), dtype=np.uint8) x[1, 1] = 255 From 4dcf8528bfc982f049d13d7074de75a6ef9c9e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Sat, 14 Jul 2012 09:30:52 +0200 Subject: [PATCH 031/648] change interface of transformation merging --- skimage/transform/geometric.py | 16 ++-------------- skimage/transform/tests/test_geometric.py | 1 - 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index 06b0839e..6dffeb8a 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -105,18 +105,15 @@ class GeometricTransformation(object): self.inverse_matrix = np.linalg.inv(self.matrix) return geometric_transform(coords, self.inverse_matrix) - def union(self, other): + def __mul__(self, other): if type(self) == type(other): transformation = self.__class__ else: transformation = GeometricTransformation return transformation(self.matrix.dot(other.matrix)) - def __mul__(self, other): - return self.union(other) - def __add__(self, other): - return self.union(other) + return self.__mul__(other) class SimilarityTransformation(GeometricTransformation): @@ -424,15 +421,6 @@ class PolynomialTransformation(GeometricTransformation): 'parameters by exchanging source and destination coordinates.' 'Then apply the forward transformation.') - def union(self, other): - raise Exception('Cannot unite polynomial transformations.') - - def __mul__(self, other): - return self.union(self, other) - - def __add__(self, other): - return self.union(self, other) - TRANSFORMATIONS = { 'similarity': SimilarityTransformation, diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index 14e839f0..b797942e 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -135,7 +135,6 @@ def test_union(): translation2 = (0, 0) tform2.from_params(scale2, rotation2, translation2) - tform = tform1.union(tform2) tform = tform1 + tform2 tform = tform1 * tform2 From 9dbad0023caf6f523c5fd3471ab670653cd4ed62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Sat, 14 Jul 2012 09:50:36 +0200 Subject: [PATCH 032/648] add support for using transformation objects in warp function --- skimage/transform/geometric.py | 10 +++++++--- skimage/transform/tests/test_geometric.py | 24 ++++++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index 6dffeb8a..2e8e3ab8 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -495,10 +495,12 @@ def warp(image, reverse_map=None, map_args={}, output_shape=None, order=1, ---------- image : 2-D array Input image. - reverse_map : callable xy = f(xy, **kwargs) - Reverse coordinate map. A function that transforms a Px2 array of + reverse_map : transformation object, callable xy = f(xy, **kwargs) + Reverse coordinate map. A function that transforms a Px2 array of ``(x, y)`` coordinates in the *output image* into their corresponding - coordinates in the *source image*. Also see examples below. + coordinates in the *source image*. In case of a transformation object + its `reverse` method will be used as transformation function. Also see + examples below. map_args : dict, optional Keyword arguments passed to `reverse_map`. output_shape : tuple (rows, cols) @@ -548,6 +550,8 @@ def warp(image, reverse_map=None, map_args={}, output_shape=None, order=1, # Map each (x, y) pair to the source image according to # the user-provided mapping + if callable(getattr(reverse_map, 'reverse', None)): + reverse_map = reverse_map.reverse tf_coords = reverse_map(tf_coords, **map_args) # Reshape back to a (2, M, N) coordinate grid diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index b797942e..dd911529 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -2,10 +2,9 @@ import numpy as np from numpy.testing import assert_array_almost_equal from skimage.transform.geometric import _stackcopy -from skimage.transform import estimate_transformation, \ - SimilarityTransformation, AffineTransformation, ProjectiveTransformation, \ - PolynomialTransformation -from skimage.transform import homography, fast_homography +from skimage.transform import estimate_transformation, homography, warp, \ + fast_homography, SimilarityTransformation, AffineTransformation, \ + ProjectiveTransformation, PolynomialTransformation from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray @@ -142,8 +141,23 @@ def test_union(): assert_array_almost_equal(tform.rotation, rotation1 + rotation2) +def test_warp(): + x = np.zeros((5, 5), dtype=np.uint8) + x[2, 2] = 255 + x = img_as_float(x) + theta = -np.pi/2 + tform = SimilarityTransformation() + tform.from_params(1, theta, (0, 4)) + + x90 = warp(x, tform, order=1) + assert_array_almost_equal(x90, np.rot90(x)) + + x90 = warp(x, tform.reverse, order=1) + assert_array_almost_equal(x90, np.rot90(x)) + + def test_homography(): - x = np.zeros((5,5), dtype=np.uint8) + x = np.zeros((5, 5), dtype=np.uint8) x[1, 1] = 255 x = img_as_float(x) theta = -np.pi/2 From 5feafee2203d743b68b6c3c0204629cfcad8314d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Sat, 14 Jul 2012 10:06:21 +0200 Subject: [PATCH 033/648] extend doc string example for geometric transformations --- skimage/transform/geometric.py | 31 ++++++++++++++--------- skimage/transform/tests/test_geometric.py | 1 - 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index 2e8e3ab8..3e67cbeb 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -105,15 +105,12 @@ class GeometricTransformation(object): self.inverse_matrix = np.linalg.inv(self.matrix) return geometric_transform(coords, self.inverse_matrix) - def __mul__(self, other): + def __add__(self, other): if type(self) == type(other): transformation = self.__class__ else: transformation = GeometricTransformation - return transformation(self.matrix.dot(other.matrix)) - - def __add__(self, other): - return self.__mul__(other) + return transformation(other.matrix.dot(self.matrix)) class SimilarityTransformation(GeometricTransformation): @@ -462,17 +459,27 @@ def estimate_transformation(ttype, src, dst, order=None): Examples -------- >>> import numpy as np - >>> from skimage.transform import make_tform + >>> from skimage import transform as tf + >>> # estimate transformation parameters >>> src = np.array([0, 0, 10, 10]).reshape((2, 2)) >>> dst = np.array([12, 14, 1, -20]).reshape((2, 2)) - >>> tform = estimate_transformation('similarity', src, dst) - >>> print tform.matrix - >>> print tform.reverse(tform.forward(src)) # == src - >>> # warp image using the transformation + >>> tform = tf.estimate_transformation('similarity', src, dst) + >>> tform.matrix + >>> tform.reverse(tform.forward(src)) # == src + >>> # warp image using the estimated transformation >>> from skimage import data >>> image = data.camera() - >>> warp(image, reverse_map=tform.forward) - >>> warp(image, reverse_map=tform.reverse) + >>> tf.warp(image, tform) # == warp(image, reverse_map=tform.reverse) + >>> tf.warp(image, reverse_map=tform.forward) + >>> # create transformation with explicit parameters + >>> tform2 = tf.SimilarityTransformation() + >>> scale = 1.1 + >>> rotation = 1 + >>> translation = (10, 20) + >>> tform2.from_params(scale, rotation, translation) + >>> # unite transformations, applied in order from left to right + >>> tform3 = tform + tform2 + >>> tform3.forward(src) # == tform2.forward(tform.forward(src)) """ ttype = ttype.lower() diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index dd911529..1648e6f1 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -135,7 +135,6 @@ def test_union(): tform2.from_params(scale2, rotation2, translation2) tform = tform1 + tform2 - tform = tform1 * tform2 assert_array_almost_equal(tform.scale, scale1 * scale2) assert_array_almost_equal(tform.rotation, rotation1 + rotation2) From d7b2c5b51b28195f75c4c767ddbb214af4522e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Sat, 14 Jul 2012 10:13:10 +0200 Subject: [PATCH 034/648] add missing doc string for polynomial forward transformation --- skimage/transform/geometric.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index 3e67cbeb..edb3d014 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -395,6 +395,19 @@ class PolynomialTransformation(GeometricTransformation): self.coeffs = np.linalg.lstsq(A, b)[0] def forward(self, coords): + """Apply forward transformation. + + Parameters + ---------- + coords : Nx2 array + source coordinates + + Returns + ------- + coords : Nx2 array + transformed coordinates + + """ x = coords[:, 0] y = coords[:, 1] u = len(self.coeffs) From afb479d766ccb97291acae27afd484a63c28c7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Sun, 15 Jul 2012 17:51:34 +0200 Subject: [PATCH 035/648] geometric_transform can transform single coordinate tuple --- skimage/transform/geometric.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py index edb3d014..2051e241 100644 --- a/skimage/transform/geometric.py +++ b/skimage/transform/geometric.py @@ -43,13 +43,22 @@ def geometric_transform(coords, matrix): coords : Nx2 array transformed coordinates """ + coords = np.asarray(coords) + shape = coords.shape + if shape == (2,): + coords = np.array([coords]) + x, y = np.transpose(coords) src = np.vstack((x, y, np.ones_like(x))) dst = np.dot(src.transpose(), matrix.transpose()) # rescale to homogeneous coordinates dst[:, 0] /= dst[:, 2] dst[:, 1] /= dst[:, 2] - return dst[:, :2] + + if shape == (2,): + return dst[0, :2] + else: + return dst[:, :2] class GeometricTransformation(object): From 8e8e2b99a0fc0ce572b31451078628709a80e7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Scho=CC=88nberger?= Date: Sun, 15 Jul 2012 19:03:44 +0200 Subject: [PATCH 036/648] add short tutorial for geometric transformations --- doc/examples/applications/plot_geometric.py | 134 ++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 doc/examples/applications/plot_geometric.py diff --git a/doc/examples/applications/plot_geometric.py b/doc/examples/applications/plot_geometric.py new file mode 100644 index 00000000..90fbce51 --- /dev/null +++ b/doc/examples/applications/plot_geometric.py @@ -0,0 +1,134 @@ +""" +=============================== +Using geometric transformations +=============================== + +In this example, we will see how to use geometric transformations in the context +of image processing. +""" + +import math +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data +from skimage import transform as tf + +margins = dict(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) + +""" +Basics +====== + +Several different geometric transformation types are supported: similarity, +affine, projective and polynomial. + +Geometric transformations can either be created using the explicit parameters +(e.g. scale, shear, rotation and translation) or the transformation matrix: +""" + +#: create using explicit parameters +tform = tf.SimilarityTransformation() +scale = 1 +rotation = math.pi/2 +translation = (0, 1) +tform.from_params(scale, rotation, translation) +print tform.matrix + +#: create using transformation matrix +matrix = tform.matrix.copy() +matrix[1, 2] = 2 +tform2 = tf.SimilarityTransformation(matrix) + +""" +These transformation objects can be used to forward and reverse transform +coordinates between the source and destination coordinate systems: +""" + +coord = [1, 0] +print tform2.forward(coord) +print tform2.reverse(tform.forward(coord)) + +""" +Image warping +============= + +Geometric transformations can also be used to warp images: +""" + +text = data.text() +tform.from_params(1, math.pi/4, (text.shape[0] / 2, -100)) + +# uses tform.reverse, alternatively use tf.warp(text, tform.reverse) +rotated = tf.warp(text, tform) +back_rotated = tf.warp(rotated, tform.forward) + +plt.figure(figsize=(8, 3)) +plt.subplot(131) +plt.imshow(text) +plt.axis('off') +plt.gray() +plt.subplot(132) +plt.imshow(rotated) +plt.axis('off') +plt.gray() +plt.subplot(133) +plt.imshow(back_rotated) +plt.axis('off') +plt.gray() +plt.subplots_adjust(**margins) + +""" +.. image:: PLOT2RST.current_figure + +Parameter estimation +==================== + +In addition to the basic functionality mentioned above you can also estimate the +parameters of a geometric transformation using the least-squares method. + +This can amongst other things be used for image registration or rectification, +where you have a set of control points or homologous points in two images. + +Let's assume we want to recognize letters on a photograph which was not taken +from the front but at a certain angle. In the simplest case of a plane paper +surface the letters are projectively distorted. Simple matching algorithms would +not be able to match such symbols. One solution to this problem would be to warp +the image so that the distortion is removed and then apply a matching algorithm: +""" + +text = data.text() + +src = np.array(( + (155, 15), + (65, 40), + (260, 130), + (360, 95) +)) +dst = np.array(( + (0, 0), + (0, 50), + (300, 50), + (300, 0) +)) + +tform3 = tf.estimate_transformation('projective', src, dst) +warped = tf.warp(text, tform3, output_shape=(50, 300)) + +plt.figure(figsize=(8, 3)) +plt.subplot(211) +plt.imshow(text) +plt.plot(src[:, 0], src[:, 1], '.r') +plt.axis('off') +plt.gray() +plt.subplot(212) +plt.imshow(warped) +plt.axis('off') +plt.gray() +plt.subplots_adjust(**margins) + +""" +.. image:: PLOT2RST.current_figure +""" + +plt.show() From a80388c99557615bbc141105588563c600aae000 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Tue, 17 Jul 2012 17:37:08 -0400 Subject: [PATCH 037/648] PKG: Update Debian packaging instructions. --- RELEASE.txt | 48 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/RELEASE.txt b/RELEASE.txt index 326a5900..d485ae3c 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -5,12 +5,9 @@ How to make a new release of ``skimage`` - Update the version number in setup.py and bento.info and commit - Update the docs: - Edit ``doc/source/themes/agogo/static/docversions.js`` and commit - - Build a clean version of the docs: run "make" in the root dir, then + - Build a clean version of the docs. Run "make" in the root dir, then ``rm build -rf; make html`` in the docs. - - Push upstream using "make gh-pages; cd gh-pages; git push" - (Note: the version list won't display correctly until after you've also - rebuilt the dev docs later on; they're grabbed from - ``skimage.org/docs/dev/.../docversions.js``.) + - Push upstream using "make gh-pages" - Add the version number as a tag in git:: git tag v0.6 @@ -24,8 +21,7 @@ How to make a new release of ``skimage`` python setup.py register python setup.py sdist upload -- Increase the version number in the setup.py and bento.info file to ``0.Xdev`` - and ``0.X.0.dev`` respectively. +- Increase the version number in the setup.py file to ``0.Xdev``. - Update the web frontpage: The webpage is kept in a separate repo: scikits-image-web @@ -34,4 +30,40 @@ How to make a new release of ``skimage`` - Post release notes on mailing lists, blog, G+, etc. -- Regenerate the dev docs, upload. +Debian +------ + +- Tag the release as per instructions above. +- git checkout debian +- git merge v0.x.x +- uscan <- not sure if this step is necessary +- Update changelog (emacs has a good mode, requires package dpkg-dev-el) + - C-C C-v add new version, C-c C-c timestamp / save +- git commit -m 'Changelog entry for 0.x.x' +- git-buildpackage -uc -us -rfakeroot +- Sign the changes: debsign skimage_0.x.x-x_amd64.changes +- cd ../build-area && dput mentors skimage_0.x.x-x_amd64.changes +- The package should now be available at: + + http://mentors.debian.net/package/skimage + +For the last lines above to work, you need ``~/.gbp.conf``:: + + [DEFAULT] + upstream-tag = %(version)s + + [git-buildpackage] + sign-tags = True + export-dir = ../build-area/ + tarball-dir = ../tarballs/ + +As well as ``~/dput.cf``:: + + [mentors] + fqdn = mentors.debian.net + incoming = /upload + method = http + allow_unsigned_uploads = 0 + progress_indicator = 2 + # Allow uploads for UNRELEASED packages + allowed_distributions = .* From f6066539ccd727a3b3613fbd0d0cd16c02a5dd27 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 19 Jul 2012 14:17:36 -0500 Subject: [PATCH 038/648] DOC: Add list of tasks for SciPy 2012 sprints --- TASKS.txt | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/TASKS.txt b/TASKS.txt index 24cb31eb..03bb444e 100644 --- a/TASKS.txt +++ b/TASKS.txt @@ -1,5 +1,57 @@ .. role:: strike + +SciPy 2012 Sprint +================= + +Welcome! Stefan van der Walt and Tony Yu are organizing a coding sprint for +scikits-image at SciPy 2012. Anyone who's interested can join the party on July 20th starting at 9 AM (location to be determined). + +We have a list of tasks for all levels of programmers, but we'd be really +interested in new ideas as well. + +Basic +----- + +These tasks should just require some basic Python knowledge. + +Code review +``````````` + +* geometric transformation PR +* morphological reconstruction PR +* Testing visualization tools + +Docs +```` + +* Task-based examples (`Where's Waldo`_, `Flatten Sudoku Puzzle`_) +* Organize/add-topics to user guide (Add overview of packages) + +.. _Flatten Sudoku Puzzle: http://stackoverflow.com/questions/10196198/how-to-remove-convexity-defects-in-sudoku-square/11366549#11366549 +.. _Where's Waldo: http://stackoverflow.com/questions/8479058/how-do-i-find-waldo-with-mathematica + +Features +```````` + +* Add text, anti-aliasing to the draw module +* Lab color space conversion +* Add slicing to ImageCollection object +* Add imread_collection to all imread backends +* Add an htmlrepr to the Image object, and return Image objects from all I/O routines? (allows automatic display of images in IPython notebook) +* Add @greyimage decorator to check if input is a greyscale image + +Intermediate +------------ + +These tasks may require some understanding of image processing algorithms or +scikits-image internals. + +* Add binary features (BRIEF, BRISK, FREAK) +* Blurring kernel estimation +* Better video loading (move to plugin framework, add backends) + + .. _howto_contribute: How to contribute to ``skimage`` From 576c6b59fa68afa09f9a27d3dbe3054a54fee262 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 20 Jul 2012 12:55:06 -0400 Subject: [PATCH 039/648] DOC: Add SciPy as a runtime dependency. --- DEPENDS.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index 2792870b..b2858256 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -4,10 +4,13 @@ Build Requirements * `Numpy >= 1.6 `__ * `Cython >= 0.15 `__ - `Matplotlib >= 1.0 `__ is needed to generate the examples in the documentation. +Runtime requirements +-------------------- +* `SciPy >= 0.10 `__ + Known build errors ------------------ On Windows, the error ``Error:unable to find vcvarsall.bat`` means that @@ -34,3 +37,4 @@ functionality is only available with the following installed: `FreeImage `__ The ``freeimage`` plugin provides support for reading various types of image file formats, including multi-page TIFFs. + From ce489a055f9ed3d3c6a1942db870732940b893e6 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 20 Jul 2012 12:56:06 -0400 Subject: [PATCH 040/648] DOC: Update tasks. --- TASKS.txt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/TASKS.txt b/TASKS.txt index 03bb444e..a20cbddb 100644 --- a/TASKS.txt +++ b/TASKS.txt @@ -5,7 +5,8 @@ SciPy 2012 Sprint ================= Welcome! Stefan van der Walt and Tony Yu are organizing a coding sprint for -scikits-image at SciPy 2012. Anyone who's interested can join the party on July 20th starting at 9 AM (location to be determined). +scikits-image at SciPy 2012. Anyone who's interested can join the party on July +20th starting at 9 AM (location to be determined). We have a list of tasks for all levels of programmers, but we'd be really interested in new ideas as well. @@ -38,7 +39,11 @@ Features * Lab color space conversion * Add slicing to ImageCollection object * Add imread_collection to all imread backends -* Add an htmlrepr to the Image object, and return Image objects from all I/O routines? (allows automatic display of images in IPython notebook) +* Resurrect the `Image object `__ and add + EXIF and TIFF tags. + - Add IPython display protocol. + - Add an htmlrepr to the Image object. Should we return image objects from + all I/O routines? * Add @greyimage decorator to check if input is a greyscale image Intermediate @@ -48,7 +53,9 @@ These tasks may require some understanding of image processing algorithms or scikits-image internals. * Add binary features (BRIEF, BRISK, FREAK) -* Blurring kernel estimation +* Add `STAR features `__ +* Using the visualization tools, add an FFT-domain image editor +* `Blurring kernel estimation `__ * Better video loading (move to plugin framework, add backends) From 02a26db4534050c682736060c4a35b9f7badf8df Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 12:26:17 -0500 Subject: [PATCH 041/648] remove message about not being able to load nose; fixes #218 --- skimage/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index a3deb6c6..c760690c 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -72,7 +72,6 @@ def _setup_test(verbose=False): try: import nose as _nose except ImportError: - print("Could not load nose. Unit tests not available.") return None else: f = functools.partial(_nose.run, 'skimage', argv=args) From 81764f693b6df82d99a3a7b325cb228aea0b0f6a Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 14:02:36 -0500 Subject: [PATCH 042/648] ENH: Add orientation kwarg to IntelligentSlider --- skimage/io/_plugins/q_color_mixer.py | 35 +++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/skimage/io/_plugins/q_color_mixer.py b/skimage/io/_plugins/q_color_mixer.py index 3fe9e29c..085bd751 100644 --- a/skimage/io/_plugins/q_color_mixer.py +++ b/skimage/io/_plugins/q_color_mixer.py @@ -1,13 +1,15 @@ # the module for the qt color_mixer plugin from PyQt4 import QtGui, QtCore from PyQt4.QtGui import (QWidget, QStackedWidget, QSlider, QGridLayout, QLabel) +from PyQt4.QtCore import Qt from util import ColorMixer + class IntelligentSlider(QWidget): ''' A slider that adds a 'name' attribute and calls a callback - with 'name' as an argument to the registerd callback. + with 'name' as an argument to the registered callback. This allows you to create large groups of sliders in a loop, but still keep track of the individual events @@ -17,7 +19,7 @@ class IntelligentSlider(QWidget): The range of the slider is hardcoded from zero - 1000, but it supports a conversion factor so you can scale the results''' - def __init__(self, name, a, b, callback): + def __init__(self, name, a, b, callback, orientation='vertical'): QWidget.__init__(self) self.name = name self.callback = callback @@ -25,24 +27,41 @@ class IntelligentSlider(QWidget): self.b = b self.manually_triggered = False - self.slider = QSlider() + if orientation == 'vertical': + orientation_slider = Qt.Vertical + alignment = QtCore.Qt.AlignHCenter + align_text = QtCore.Qt.AlignCenter + align_value = QtCore.Qt.AlignCenter + elif orientation == 'horizontal': + orientation_slider = Qt.Horizontal + alignment = QtCore.Qt.AlignVCenter + align_text = QtCore.Qt.AlignLeft + align_value = QtCore.Qt.AlignRight + + self.slider = QSlider(orientation_slider) self.slider.setRange(0, 1000) self.slider.setValue(500) self.slider.valueChanged.connect(self.slider_changed) self.name_label = QLabel() self.name_label.setText(self.name) - self.name_label.setAlignment(QtCore.Qt.AlignCenter) + self.name_label.setAlignment(align_text) self.value_label = QLabel() self.value_label.setText('%2.2f' % (self.slider.value() * self.a + self.b)) - self.value_label.setAlignment(QtCore.Qt.AlignCenter) + self.value_label.setAlignment(align_value) self.layout = QGridLayout(self) - self.layout.addWidget(self.name_label, 0, 0) - self.layout.addWidget(self.slider, 1, 0, QtCore.Qt.AlignHCenter) - self.layout.addWidget(self.value_label, 2, 0) + + if orientation == 'vertical': + self.layout.addWidget(self.name_label, 0, 0) + self.layout.addWidget(self.slider, 1, 0, alignment) + self.layout.addWidget(self.value_label, 2, 0) + elif orientation == 'horizontal': + self.layout.addWidget(self.name_label, 0, 0) + self.layout.addWidget(self.slider, 0, 1, alignment) + self.layout.addWidget(self.value_label, 0, 2) # bind this to the valueChanged signal of the slider def slider_changed(self, val): From c27119b0cd919a23266c3a4581779c18c0dbf9bf Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 14:03:47 -0500 Subject: [PATCH 043/648] ENH: Add image viewer based on Qt and Matplotlib --- skimage/viewer/__init__.py | 1 + skimage/viewer/plugins/__init__.py | 0 skimage/viewer/plugins/base.py | 70 ++++++++++++++++++ skimage/viewer/plugins/canny.py | 20 ++++++ skimage/viewer/utils/__init__.py | 1 + skimage/viewer/utils/core.py | 75 +++++++++++++++++++ skimage/viewer/viewers/__init__.py | 1 + skimage/viewer/viewers/core.py | 96 +++++++++++++++++++++++++ viewer_examples/plugins/canny.py | 10 +++ viewer_examples/viewers/image_viewer.py | 7 ++ 10 files changed, 281 insertions(+) create mode 100644 skimage/viewer/__init__.py create mode 100644 skimage/viewer/plugins/__init__.py create mode 100644 skimage/viewer/plugins/base.py create mode 100644 skimage/viewer/plugins/canny.py create mode 100644 skimage/viewer/utils/__init__.py create mode 100644 skimage/viewer/utils/core.py create mode 100644 skimage/viewer/viewers/__init__.py create mode 100644 skimage/viewer/viewers/core.py create mode 100644 viewer_examples/plugins/canny.py create mode 100644 viewer_examples/viewers/image_viewer.py diff --git a/skimage/viewer/__init__.py b/skimage/viewer/__init__.py new file mode 100644 index 00000000..e20546b0 --- /dev/null +++ b/skimage/viewer/__init__.py @@ -0,0 +1 @@ +from viewers import ImageViewer diff --git a/skimage/viewer/plugins/__init__.py b/skimage/viewer/plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py new file mode 100644 index 00000000..a92140cb --- /dev/null +++ b/skimage/viewer/plugins/base.py @@ -0,0 +1,70 @@ +from PyQt4 import QtGui +from skimage.io._plugins.q_color_mixer import IntelligentSlider + + +class Plugin(QtGui.QDialog): + """Base class for widgets that interact with the axes. + + Parameters + ---------- + image_viewer : ImageViewer instance. + Window containing image used in measurement/manipulation. + useblit : bool + If True, use blitting to speed up animation. Only available on some + backends. If None, set to True when using Agg backend, otherwise False. + figure : :class:`~matplotlib.figure.Figure` + If None, create a figure with a single axes. + no_toolbar : bool + If True, figure created by plugin has no toolbar. This has no effect + on figures passed into `Plugin`. + + Attributes + ---------- + viewer : ImageViewer + Window containing image used in measurement. + image : array + Image used in measurement/manipulation. + overlay : array + Image used in measurement/manipulation. + """ + def __init__(self, callback, parent=None, height=100, width=400): + self._viewer = parent + QtGui.QDialog.__init__(self, parent) + self.setWindowTitle('Image Plugin') + self.layout = QtGui.QGridLayout(self) + self.resize(width, height) + self.row = 0 + self.callback = callback + + self.arguments = [parent.original_image] + self.keyword_arguments= {} + + self.overlay = self._viewer.overlay + self.image = self._viewer.image + + def caller(self, *args): + arguments = [self._get_value(a) for a in self.arguments] + kwargs = dict([(name, self._get_value(a)) + for name, a in self.keyword_arguments.iteritems()]) + self.callback(*arguments, **kwargs) + + def _get_value(self, param): + if hasattr(param, 'val'): + return param.val() + else: + return param + + def add_argument(self, name, low, high, callback): + name, slider = self.add_slider(name, low, high, callback) + self.arguments[name] = slider + + def add_keyword_argument(self, name, low, high, callback): + name, slider = self.add_slider(name, low, high, callback) + self.keyword_arguments[name] = slider + + def add_slider(self, name, low, high, callback): + slider = IntelligentSlider(name, low, high, callback, + orientation='horizontal') + self.layout.addWidget(slider, self.row, 0) + self.row += 1 + return name.replace(' ', '_'), slider diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py new file mode 100644 index 00000000..cf417a10 --- /dev/null +++ b/skimage/viewer/plugins/canny.py @@ -0,0 +1,20 @@ +from .base import Plugin +from skimage.filter import canny + + +class CannyPlugin(Plugin): + + def __init__(self, parent, *args, **kwargs): + height = kwargs.get('height', 100) + width = kwargs.get('width', 400) + super(CannyPlugin, self).__init__(self.callback, parent=parent, + width=width, height=height) + self.add_keyword_argument('sigma', 0.005, 0, self.caller) + self.add_keyword_argument('low_threshold', 0.255, 0, self.caller) + self.add_keyword_argument('high_threshold', 0.255, 0, self.caller) + # Call callback so that image is updated to slider values. + self.caller() + + def callback(self, *args, **kwargs): + image = canny(*args, **kwargs) + self._viewer.overlay = image diff --git a/skimage/viewer/utils/__init__.py b/skimage/viewer/utils/__init__.py new file mode 100644 index 00000000..5af24064 --- /dev/null +++ b/skimage/viewer/utils/__init__.py @@ -0,0 +1 @@ +from core import * diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py new file mode 100644 index 00000000..efad540d --- /dev/null +++ b/skimage/viewer/utils/core.py @@ -0,0 +1,75 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.colors import LinearSegmentedColormap + + +__all__ = ['figimage', 'LinearColormap', 'ClearColormap', 'clear_red'] + + +def figimage(image, scale=1, dpi=None, **kwargs): + """Return figure and axes with figure tightly surrounding image. + + Unlike pyplot.figimage, this actually plots onto an axes object, which + fills the figure. Plotting the image onto an axes allows for subsequent + overlays of axes artists. + + Parameters + ---------- + image : array + image to plot + scale : float + If scale is 1, the figure and axes have the same dimension as the + image. Smaller values of `scale` will shrink the figure. + dpi : int + Dots per inch for figure. If None, use the default rcParam. + """ + dpi = dpi if dpi is not None else plt.rcParams['figure.dpi'] + kwargs.setdefault('interpolation', 'nearest') + kwargs.setdefault('cmap', 'gray') + + h, w, d = np.atleast_3d(image).shape + figsize = np.array((w, h), dtype=float) / dpi * scale + + fig, ax = plt.subplots(figsize=figsize, dpi=dpi) + fig.subplots_adjust(left=0, bottom=0, right=1, top=1) + + ax.set_axis_off() + ax.imshow(image, **kwargs) + return fig, ax + + +class LinearColormap(LinearSegmentedColormap): + """LinearSegmentedColormap in which color varies smoothly. + + This class is a simplification of LinearSegmentedColormap, which doesn't + support jumps in color intensities. + + Parameters + ---------- + name : str + Name of colormap. + + segmented_data : dict + Dictionary of 'red', 'green', 'blue', and (optionally) 'alpha' values. + Each color key contains a list of `x`, `y` tuples. `x` must increase + monotonically from 0 to 1 and corresponds to input values for a mappable + object (e.g. an image). `y` corresponds to the color intensity. + + """ + def __init__(self, name, segmented_data, **kwargs): + segmented_data = dict((key, [(x, y, y) for x, y in value]) + for key, value in segmented_data.iteritems()) + LinearSegmentedColormap.__init__(self, name, segmented_data, **kwargs) + + +class ClearColormap(LinearColormap): + def __init__(self, name, rgb): + r, g, b = rgb + cg_speq = {'blue': [(0.0, b), (1.0, b)], + 'green': [(0.0, g), (1.0, g)], + 'red': [(0.0, r), (1.0, r)], + 'alpha': [(0.0, 0.0), (1.0, 1.0)]} + LinearColormap.__init__(self, name, cg_speq) + +clear_red = ClearColormap('clear_red', (0.7, 0, 0)) + diff --git a/skimage/viewer/viewers/__init__.py b/skimage/viewer/viewers/__init__.py new file mode 100644 index 00000000..bb67a43f --- /dev/null +++ b/skimage/viewer/viewers/__init__.py @@ -0,0 +1 @@ +from .core import * diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py new file mode 100644 index 00000000..e1c4d003 --- /dev/null +++ b/skimage/viewer/viewers/core.py @@ -0,0 +1,96 @@ +import sys + +from PyQt4 import QtGui, QtCore +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg + +from skimage.viewer.utils import figimage, clear_red + + +qApp = None + + +class ImageCanvas(FigureCanvasQTAgg): + """Canvas for displaying images. + + This canvas derives from Matplotlib, so your normal + """ + def __init__(self, parent, image, **kwargs): + self.fig, self.ax = figimage(image, **kwargs) + + FigureCanvasQTAgg.__init__(self, self.fig) + FigureCanvasQTAgg.setSizePolicy(self, + QtGui.QSizePolicy.Expanding, + QtGui.QSizePolicy.Expanding) + FigureCanvasQTAgg.updateGeometry(self) + # Note: `setParent` must be called after `FigureCanvasQTAgg.__init__`. + self.setParent(parent) + + +class ImageViewer(QtGui.QMainWindow): + + def __init__(self, image): + # Start main loop + global qApp + if qApp is None: + qApp = QtGui.QApplication(sys.argv) + super(ImageViewer, self).__init__() + + #TODO: Add ImageViewer to skimage.io window manager + + self.overlay_cmap = clear_red + + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setWindowTitle("Image Viewer") + + self.file_menu = QtGui.QMenu('&File', self) + self.file_menu.addAction('&Quit', self.close, + QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + self.menuBar().addMenu(self.file_menu) + + self.main_widget = QtGui.QWidget() + self.setCentralWidget(self.main_widget) + + self.canvas = ImageCanvas(self.main_widget, image) + self.fig = self.canvas.fig + self.ax = self.canvas.ax + + self.layout = QtGui.QVBoxLayout(self.main_widget) + self.layout.addWidget(self.canvas) + + #TODO: Add coordinate display + # self.statusBar().showMessage("coordinates") + self.original_image = image + self.image = image + self._overlay = None + + @property + def image(self): + return self._img + + @image.setter + def image(self, image): + self._img = image + self.ax.images[0].set_array(image) + self.canvas.draw_idle() + + @property + def overlay(self): + return self._overlay + + @overlay.setter + def overlay(self, image): + self._overlay = image + if len(self.ax.images) == 1: + self.ax.imshow(image, cmap=self.overlay_cmap) + else: + self.ax.images[1].set_array(image) + self.canvas.draw_idle() + + def closeEvent(self, ce): + self.close() + + def show(self): + super(ImageViewer, self).show() + sys.exit(qApp.exec_()) + + diff --git a/viewer_examples/plugins/canny.py b/viewer_examples/plugins/canny.py new file mode 100644 index 00000000..d5e858ec --- /dev/null +++ b/viewer_examples/plugins/canny.py @@ -0,0 +1,10 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.canny import CannyPlugin + + +image = data.camera() +viewer = ImageViewer(image) +p = CannyPlugin(viewer) +p.show() +viewer.show() diff --git a/viewer_examples/viewers/image_viewer.py b/viewer_examples/viewers/image_viewer.py new file mode 100644 index 00000000..e0932787 --- /dev/null +++ b/viewer_examples/viewers/image_viewer.py @@ -0,0 +1,7 @@ +from skimage import data +from skimage.viewer import ImageViewer + + +image = data.camera() +viewer = ImageViewer(image) +viewer.show() From 89f0151a7a4329007b6ac6e93194466f779bb10f Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Fri, 20 Jul 2012 15:05:45 -0400 Subject: [PATCH 044/648] BUG: Fix testing failures when FreeImage not installed. FreeImage throws an OSError, which must be caught when attempting to load the IO plugins for tests to pass without FreeImage installed. --- skimage/io/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/__init__.py b/skimage/io/__init__.py index c5dd0b77..5e701a51 100644 --- a/skimage/io/__init__.py +++ b/skimage/io/__init__.py @@ -31,7 +31,7 @@ def _load_preferred_plugins(): try: use_plugin(plugin, kind=func) break - except (ImportError, RuntimeError): + except (ImportError, RuntimeError, OSError): pass # Use PIL as the default imread plugin, since matplotlib (1.2.x) From 0036dc9775de0260d56ac55197c9fbb812ec9113 Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Fri, 20 Jul 2012 15:09:18 -0400 Subject: [PATCH 045/648] ImageCollection now supports slicing --- skimage/io/collection.py | 54 +++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index e698b52b..e83a5b18 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -5,6 +5,7 @@ from __future__ import with_statement __all__ = ['MultiImage', 'ImageCollection', 'imread'] from glob import glob +from copy import copy import numpy as np from ._io import imread @@ -250,29 +251,58 @@ class ImageCollection(object): return self._conserve_memory def __getitem__(self, n): - """Return image n in the collection. + """Return selected image(s) in the collection. Loading is done on demand. Parameters ---------- - n : int - The image number to be returned. + n : int or slice + Slice selecting images for the new ImageCollection or the image + number to be returned. Returns ------- - img : ndarray - The `n`-th image in the collection. + img : ImageCollection or ndarray + Imagecollection of the selected images or an ndarray if a single + image is specified. """ - n = self._check_imgnum(n) - idx = n % len(self.data) + if type(n) not in [int, slice]: + raise TypeError('slicing must be an int or slice object') + + if type(n) is int: + n = self._check_imgnum(n) + idx = n % len(self.data) - if (self.conserve_memory and n != self._cached) or \ - (self.data[idx] is None): - self.data[idx] = self.load_func(self.files[n]) - self._cached = n + if (self.conserve_memory and n != self._cached) or \ + (self.data[idx] is None): + self.data[idx] = self.load_func(self.files[n]) + self._cached = n - return self.data[idx] + return self.data[idx] + + else: # slice object was provided + fidx = range(len(self.files))[n] + fidx.sort() + if len(fidx) == 1: # only one item requested + return self.__getitem__(fidx[0]) + else: + # create a new ImageCollection object, any loaded image data + # in the original ImageCollection will be copied by reference + # to the new object. Image data loaded after this creation + # are not linked. + newIC = copy(self) + newIC._files = [self.files[i] for i in fidx] + if self.conserve_memory: + if self._cached in fidx: + newIC._cached = fidx[self._cached] + newIC.data = np.copy(self.data) + else: + newIC.data = np.empty(1, dtype=object) + else: + newIC.data = self.data[fidx] + + return newIC def _check_imgnum(self, n): """Check that the given image number is valid.""" From 5101aa2a389c005e3be0eba22c5c5c168493fcf4 Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Fri, 20 Jul 2012 15:23:25 -0400 Subject: [PATCH 046/648] added test for ImageCollection slicing --- skimage/io/tests/test_collection.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index 0d420ae7..36e78e13 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -43,6 +43,12 @@ class TestImageCollection(): 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_array_almost_equal(self.collection[0], self.collection[:1]) + assert_array_almost_equal(self.collection[1], self.collection[1:]) + def test_files_property(self): assert isinstance(self.collection.files, list) From dceb7b6c7d80654cebb8b0aaefb1e8fd95e9b704 Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Fri, 20 Jul 2012 15:39:40 -0400 Subject: [PATCH 047/648] variable name changes and small fixes --- skimage/io/collection.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index e83a5b18..a78dbd14 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -267,8 +267,11 @@ class ImageCollection(object): Imagecollection of the selected images or an ndarray if a single image is specified. """ + if hasattr(n, '__index__'): + n = n.__index__() + if type(n) not in [int, slice]: - raise TypeError('slicing must be an int or slice object') + raise TypeError('slicing must be with an int or slice object') if type(n) is int: n = self._check_imgnum(n) @@ -283,7 +286,6 @@ class ImageCollection(object): else: # slice object was provided fidx = range(len(self.files))[n] - fidx.sort() if len(fidx) == 1: # only one item requested return self.__getitem__(fidx[0]) else: @@ -291,18 +293,19 @@ class ImageCollection(object): # in the original ImageCollection will be copied by reference # to the new object. Image data loaded after this creation # are not linked. - newIC = copy(self) - newIC._files = [self.files[i] for i in fidx] + fidx.sort() + new_ic = copy(self) + new_ic._files = [self.files[i] for i in fidx] if self.conserve_memory: if self._cached in fidx: - newIC._cached = fidx[self._cached] - newIC.data = np.copy(self.data) + new_ic._cached = fidx[self._cached] + new_ic.data = np.copy(self.data) else: - newIC.data = np.empty(1, dtype=object) + new_ic.data = np.empty(1, dtype=object) else: - newIC.data = self.data[fidx] + new_ic.data = self.data[fidx] - return newIC + return new_ic def _check_imgnum(self, n): """Check that the given image number is valid.""" From 1903ed892d60eb35e4d5dde3f0b6c66814b0b74c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 14:45:32 -0500 Subject: [PATCH 048/648] API change: switch order of image viewer and callback arguments. --- skimage/viewer/plugins/base.py | 11 ++++++----- skimage/viewer/plugins/canny.py | 4 ++-- skimage/viewer/utils/core.py | 2 ++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index a92140cb..9a8c2cda 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -27,16 +27,17 @@ class Plugin(QtGui.QDialog): overlay : array Image used in measurement/manipulation. """ - def __init__(self, callback, parent=None, height=100, width=400): - self._viewer = parent - QtGui.QDialog.__init__(self, parent) + def __init__(self, image_viewer, callback=None, height=100, width=400): + self._viewer = image_viewer + QtGui.QDialog.__init__(self, image_viewer) self.setWindowTitle('Image Plugin') self.layout = QtGui.QGridLayout(self) self.resize(width, height) self.row = 0 - self.callback = callback + if callback is not None: + self.callback = callback - self.arguments = [parent.original_image] + self.arguments = [image_viewer.original_image] self.keyword_arguments= {} self.overlay = self._viewer.overlay diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index cf417a10..db702fc9 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -4,10 +4,10 @@ from skimage.filter import canny class CannyPlugin(Plugin): - def __init__(self, parent, *args, **kwargs): + def __init__(self, image_viewer, *args, **kwargs): height = kwargs.get('height', 100) width = kwargs.get('width', 400) - super(CannyPlugin, self).__init__(self.callback, parent=parent, + super(CannyPlugin, self).__init__(image_viewer, width=width, height=height) self.add_keyword_argument('sigma', 0.005, 0, self.caller) self.add_keyword_argument('low_threshold', 0.255, 0, self.caller) diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index efad540d..f7dc6433 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -63,6 +63,8 @@ class LinearColormap(LinearSegmentedColormap): class ClearColormap(LinearColormap): + """Color map that varies linearly from alpha = 0 to 1 + """ def __init__(self, name, rgb): r, g, b = rgb cg_speq = {'blue': [(0.0, b), (1.0, b)], From db4cc04a900f3732f8409478dfd3784de67a33c0 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Fri, 20 Jul 2012 15:46:58 -0400 Subject: [PATCH 049/648] BUG: Fix tests when FreeImage is not installed. The plugin loader tries plugins but only catches ImportError and RuntimeError. The FreeImage plugin was throwing OSError. Tests were failing when FreeImage was not installed. It now throws a RuntimeError. --- skimage/io/_plugins/freeimage_plugin.py | 4 ++-- skimage/io/tests/test_freeimage.py | 4 ++-- skimage/io/tests/test_plugin.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index 6475781b..af611250 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -72,12 +72,12 @@ def load_freeimage(): # No freeimage library loaded, and load-errors reported for some # candidate libs err_txt = ['%s:\n%s' % (l, str(e.message)) for l, e in errors] - raise OSError('One or more FreeImage libraries were found, but ' + raise RuntimeError('One or more FreeImage libraries were found, but ' 'could not be loaded due to the following errors:\n' '\n\n'.join(err_txt)) else: # No errors, because no potential libraries found at all! - raise OSError('Could not find a FreeImage library in any of:\n' + + raise RuntimeError('Could not find a FreeImage library in any of:\n' + '\n'.join(lib_dirs)) # FreeImage found diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index 565f37bd..7a294f9e 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -11,7 +11,7 @@ try: import skimage.io._plugins.freeimage_plugin as fi FI_available = True sio.use_plugin('freeimage') -except OSError: +except RuntimeError: FI_available = False @@ -23,7 +23,7 @@ def setup_module(self): """ try: sio.use_plugin('freeimage') - except OSError: + except RuntimeError: pass diff --git a/skimage/io/tests/test_plugin.py b/skimage/io/tests/test_plugin.py index 28d8c2b3..5d1febe4 100644 --- a/skimage/io/tests/test_plugin.py +++ b/skimage/io/tests/test_plugin.py @@ -15,7 +15,7 @@ try: io.use_plugin('freeimage') FI_available = True priority_plugin = 'freeimage' -except OSError: +except RuntimeError: FI_available = False From 86f67f85edfbd6acd80e4ff76c15db6c060ec4de Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 14:45:51 -0500 Subject: [PATCH 050/648] raise an import error if trying to run test suite without nose --- skimage/__init__.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index c760690c..ad37f425 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -72,25 +72,20 @@ def _setup_test(verbose=False): try: import nose as _nose except ImportError: - return None + def broken_test_func(): + """This would invoke the skimage test suite, but nose couldn't be + imported so the test suite can not run. + """ + raise ImportError("Could not load nose. Unit tests not available.") + return broken_test_func else: f = functools.partial(_nose.run, 'skimage', argv=args) f.__doc__ = 'Invoke the skimage test suite.' return f -test = _setup_test() -if test is None: - try: - del test - except NameError: - pass +test = _setup_test() test_verbose = _setup_test(verbose=True) -if test_verbose is None: - try: - del test - except NameError: - pass def get_log(name=None): From 74554793ad2d894afc92cee340e75420ca95b080 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Fri, 20 Jul 2012 16:00:06 -0400 Subject: [PATCH 051/648] STY: Align multi-line string statements. --- skimage/io/_plugins/freeimage_plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index af611250..d2a9d4f9 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -73,12 +73,12 @@ def load_freeimage(): # candidate libs err_txt = ['%s:\n%s' % (l, str(e.message)) for l, e in errors] raise RuntimeError('One or more FreeImage libraries were found, but ' - 'could not be loaded due to the following errors:\n' - '\n\n'.join(err_txt)) + 'could not be loaded due to the following errors:\n' + '\n\n'.join(err_txt)) else: # No errors, because no potential libraries found at all! raise RuntimeError('Could not find a FreeImage library in any of:\n' + - '\n'.join(lib_dirs)) + '\n'.join(lib_dirs)) # FreeImage found @functype(None, ctypes.c_int, ctypes.c_char_p) From 11c7fd2f59de5443f7fbb8a6ea7e860a9ad3fb88 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 20 Jul 2012 15:16:48 -0500 Subject: [PATCH 052/648] Add function 'concatenate' to ImageCollection Many algorithms work on 3D stacks rather than images. It is convenient to provide automatic conversion from an ImageCollection to an nD-stack. --- skimage/io/collection.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index e698b52b..7a07aa7f 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -307,3 +307,23 @@ class ImageCollection(object): """ self.data = np.empty_like(self.data) + + def concatenate(self): + """Concatenate all images in the collection on a new axis. + + Returns + ------- + ar : np.ndarray + An array having one more dimension than the images in `self`. + + Raises + ------ + ValueError + If images in the collection don't have identical shapes. + """ + all_images = [img[np.newaxis, ...] for img in self] + try: + ar = np.concatenate(all_images) + except ValueError: + raise ValueError('Image dimensions must agree.') + return ar From f6761524481c163ed72e8cbdadec98776192c31f Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 15:18:09 -0500 Subject: [PATCH 053/648] add stefan's image class --- skimage/io/_io.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 477fe8bc..85e5c005 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -5,10 +5,64 @@ from skimage.io._plugins import call as call_plugin from skimage.color import rgb2grey import numpy as np + # Shared image queue _image_stack = [] +class Image(np.ndarray): + """Image data with tags.""" + + tags = {'filename': '', + 'EXIF': {}, + 'info': {}} + + def __new__(image_cls, arr, **kwargs): + """Set the image data and tags according to given parameters. + + Input: + ------ + `image_cls` : Image class specification + This is not normally specified by the user. + `arr` : ndarray + Image data. + ``**kwargs`` : Image tags as keywords + Specified in the form ``tag0=value``, ``tag1=value``. + + """ + x = np.asarray(arr).view(image_cls) + for tag, value in Image.tags.items(): + setattr(x, tag, kwargs.get(tag, getattr(arr, tag, value))) + return x + + def __array_finalize__(self, obj): + """Copy object tags.""" + for tag, value in Image.tags.items(): + setattr(self, tag, getattr(obj, tag, value)) + return + + def __reduce__(self): + object_state = list(np.ndarray.__reduce__(self)) + subclass_state = {} + for tag in self.tags: + subclass_state[tag] = getattr(self, tag) + object_state[2] = (object_state[2], subclass_state) + return tuple(object_state) + + def __setstate__(self, state): + nd_state, subclass_state = state + np.ndarray.__setstate__(self, nd_state) + + for tag in subclass_state: + setattr(self, tag, subclass_state[tag]) + + @property + def exposure(self): + """Return exposure time based on EXIF tag.""" + exposure = self.EXIF['EXIF ExposureTime'].values[0] + return exposure.num / float(exposure.den) + + def push(img): """Push an image onto the shared image stack. @@ -77,7 +131,7 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, if as_grey and getattr(img, 'ndim', 0) >= 3: img = rgb2grey(img) - return img + return Image(img) def imread_collection(load_pattern, conserve_memory=True, From d2e04848453696acb68ddfea57ab455b6c8cc9a6 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 15:19:22 -0500 Subject: [PATCH 054/648] add html repr method for Image class --- skimage/io/_io.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 85e5c005..3e5bb670 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,10 +1,17 @@ __all__ = ['imread', 'imread_collection', 'imsave', 'imshow', 'show', 'push', 'pop'] +import base64 + from skimage.io._plugins import call as call_plugin from skimage.color import rgb2grey import numpy as np +try: + import cStringIO as StringIO +except ImportError: + import StringIO + # Shared image queue _image_stack = [] @@ -49,6 +56,12 @@ class Image(np.ndarray): object_state[2] = (object_state[2], subclass_state) return tuple(object_state) + def _repr_html_(self): + str_buffer = StringIO.StringIO() + imsave(str_buffer, self, format_str='png') + base64_str = base64.b64encode(str_buffer.getvalue()) + return '' % base64_str + def __setstate__(self, state): nd_state, subclass_state = state np.ndarray.__setstate__(self, nd_state) From 605a4e2cd73e4ec1e2ff49ccd18625f06a93b1e5 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 15:22:53 -0500 Subject: [PATCH 055/648] add support for serializing to file-like objects (e.g. StringIO) to PIL plugin --- skimage/io/_plugins/pil_plugin.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 96106c5c..f42efe89 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -59,17 +59,21 @@ def _palette_is_grayscale(pil_image): return np.allclose(np.diff(valid_palette), 0) -def imsave(fname, arr): +def imsave(fname, arr, format_str=None): """Save an image to disk. Parameters ---------- - fname : str + fname : str or file-like object Name of destination file. arr : ndarray of uint8 or float Array (image) to save. Arrays of data-type uint8 should have values in [0, 255], whereas floating-point arrays must be in [0, 1]. + format_str: str + Format to save as, this is required if using a file-like object; + this is optional if fname is a string and the format can be + derived from the extension. Notes ----- @@ -101,7 +105,11 @@ def imsave(fname, arr): arr = arr.astype(np.uint8) img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) - img.save(fname) + + if isinstance(fname, basestring): + img.save(fname, format=format_str) + elif callable(getattr(fname, 'write', None)): + img.save(fname, format=format_str) def imshow(arr): From 62ce6f3ab52c9a73e5eca7b52d95df62548ae776 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 20 Jul 2012 15:35:03 -0500 Subject: [PATCH 056/648] Add test for ImageCollection.concatenate --- skimage/io/tests/test_collection.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index 0d420ae7..6ddae6fe 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -23,9 +23,12 @@ if sys.version_info[0] > 2: 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']] def setUp(self): self.collection = ImageCollection(self.pattern) + self.collection_matched = ImageCollection(self.pattern_matched) def test_len(self): assert len(self.collection) == 2 @@ -59,6 +62,12 @@ class TestImageCollection(): ic = ImageCollection(load_pattern, load_func=load_fn) 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) + class TestMultiImage(): From 70cf6cfba0040dc347a31c75de4b0b06a2bd7bd0 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 15:39:50 -0500 Subject: [PATCH 057/648] fix test so it looks for new Image class --- skimage/io/_io.py | 2 +- skimage/io/tests/test_collection.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 3e5bb670..f3184588 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,4 +1,4 @@ -__all__ = ['imread', 'imread_collection', 'imsave', 'imshow', 'show', +__all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show', 'push', 'pop'] import base64 diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index 0d420ae7..bf183f2b 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -7,6 +7,7 @@ from numpy.testing.decorators import skipif from skimage import data_dir from skimage.io import ImageCollection, MultiImage +from skimage.io import Image as ioImage try: @@ -33,7 +34,7 @@ class TestImageCollection(): def test_getitem(self): num = len(self.collection) for i in range(-num, num): - assert type(self.collection[i]) is np.ndarray + assert type(self.collection[i]) is ioImage assert_array_almost_equal(self.collection[0], self.collection[-num]) From 736b92a5ff9ca5a24c551f3f0c4c445c3e10acb0 Mon Sep 17 00:00:00 2001 From: Leon Tietz Date: Fri, 20 Jul 2012 16:31:16 -0500 Subject: [PATCH 058/648] Updated Image Segmentation tutorial --- .../user_guide/tutorial_segmentation.txt | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/doc/source/user_guide/tutorial_segmentation.txt b/doc/source/user_guide/tutorial_segmentation.txt index eda202d9..cff7c651 100644 --- a/doc/source/user_guide/tutorial_segmentation.txt +++ b/doc/source/user_guide/tutorial_segmentation.txt @@ -47,10 +47,6 @@ detection, we use the `Canny detector As the background is very smooth, almost all edges are found at the boundary of the coins, or inside the coins. -Now that we have contours that delineate the outer boundary of the coins, -we fill the inner part of the coins using the -``ndimage.binary_fill_holes`` function, which uses mathematical morphology -to fill the holes. :: @@ -61,6 +57,15 @@ to fill the holes. :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center +Now that we have contours that delineate the outer boundary of the coins, +we fill the inner part of the coins using the +``ndimage.binary_fill_holes`` function, which uses mathematical morphology +to fill the holes. + +.. image:: ../../_images/plot_coins_segmentation_4.png + :target: ../auto_examples/applications/plot_coins_segmentation.html + :align: center + Most coins are well segmented out of the background. Small objects from the background can be easily removed using the ``ndimage.label`` function to remove objects smaller than a small threshold. @@ -78,6 +83,10 @@ has not been segmented correctly at all. The reason is that the contour that we got from the Canny detector was not completely closed, therefore the filling function did not fill the inner part of the coin. +.. image:: ../../_images/plot_coins_segmentation_5.png + :target: ../auto_examples/applications/plot_coins_segmentation.html + :align: center + Therefore, this segmentation method is not very robust: if we miss a single pixel of the contour of the object, we will not be able to fill it. Of course, we could try to dilate the contours in order to @@ -117,12 +126,29 @@ separate the coins from the background. .. image:: data/elevation_map.jpg :align: center +and here is the corresponding 2-D plot: + +.. image:: ../../_images/plot_coins_segmentation_6.png + :target: ../auto_examples/applications/plot_coins_segmentation.html + :align: center + +The next step is to find markers of the background and the coins based on the +extreme parts of the histogram of grey values:: + + >>> markers = np.zeros_like(coins) + >>> markers[coins < 30] = 1 + >>> markers[coins > 150] = 2 + +.. image:: ../../_images/plot_coins_segmentation_7.png + :target: ../auto_examples/applications/plot_coins_segmentation.html + :align: center + Let us now compute the watershed transform:: >>> from skimage.morphology import watershed >>> segmentation = watershed(elevation_map, markers) -.. image:: ../../_images/plot_coins_segmentation_4.png +.. image:: ../../_images/plot_coins_segmentation_8.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center @@ -139,7 +165,7 @@ We can now label all the coins one by one using ``ndimage.label``:: >>> labeled_coins, _ = ndimage.label(segmentation) -.. image:: ../../_images/plot_coins_segmentation_5.png +.. image:: ../../_images/plot_coins_segmentation_9.png :target: ../auto_examples/applications/plot_coins_segmentation.html :align: center From 32d3e1b161a12c46fa5c52a66c4bccd705c572b3 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 16:44:05 -0500 Subject: [PATCH 059/648] use repr_png and repr_jpeg hooks rather than repr_html these will work in the ipython qtconsole as well! --- skimage/io/_io.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index f3184588..c271f25b 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -56,11 +56,15 @@ class Image(np.ndarray): object_state[2] = (object_state[2], subclass_state) return tuple(object_state) - def _repr_html_(self): + def _repr_png_(self): str_buffer = StringIO.StringIO() imsave(str_buffer, self, format_str='png') - base64_str = base64.b64encode(str_buffer.getvalue()) - return '' % base64_str + return str_buffer.getvalue() + + def _repr_jpeg_(self): + str_buffer = StringIO.StringIO() + imsave(str_buffer, self, format_str='jpeg') + return str_buffer.getvalue() def __setstate__(self, state): nd_state, subclass_state = state From a61ac37ff2e848ffafc34a95e751f6363f5a5956 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 20 Jul 2012 16:46:33 -0500 Subject: [PATCH 060/648] Add concatenate() support for MultiImage Previously we added the ImageCollection.concatenate function. This updates MultiImage to have the same functionality, and moves the grunt work to an outside function to avoid repetition. --- skimage/io/collection.py | 65 +++++++++++++++++++++++++---- skimage/io/tests/test_collection.py | 6 +++ 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 7a07aa7f..1e31bd1d 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -2,13 +2,42 @@ from __future__ import with_statement -__all__ = ['MultiImage', 'ImageCollection', 'imread'] +__all__ = ['MultiImage', 'ImageCollection', 'imread', 'concatenate_images'] from glob import glob import numpy as np from ._io import imread +def concatenate_images(ic): + """Concatenate all images in the image collection into an array. + + Parameters + ---------- + ic: an iterable of images (including ImageCollection and MultiImage) + The images to be concatenated. + + Returns + ------- + ar : np.ndarray + An array having one more dimension than the images in `ic`. + + See Also + -------- + `ImageCollection.concatenate`, `MultiImage.concatenate` + + Raises + ------ + ValueError + If images in `ic` don't have identical shapes. + """ + all_images = [img[np.newaxis, ...] for img in ic] + try: + ar = np.concatenate(all_images) + except ValueError: + raise ValueError('Image dimensions must agree.') + return ar + class MultiImage(object): """A class containing a single multi-frame image. @@ -142,6 +171,24 @@ class MultiImage(object): def __str__(self): return str(self.filename) + ' [%s frames]' % self._numframes + def concatenate(self): + """Concatenate all images in the multi-image into an array. + + Returns + ------- + ar : np.ndarray + An array having one more dimension than the images in `self`. + + See Also + -------- + `concatenate_images` + + Raises + ------ + ValueError + If images in the `MultiImage` don't have identical shapes. + """ + return concatenate_images(self) class ImageCollection(object): """Load and manage a collection of image files. @@ -309,21 +356,21 @@ class ImageCollection(object): self.data = np.empty_like(self.data) def concatenate(self): - """Concatenate all images in the collection on a new axis. + """Concatenate all images in the collection into an array. Returns ------- ar : np.ndarray An array having one more dimension than the images in `self`. + See Also + -------- + `concatenate_images` + Raises ------ ValueError - If images in the collection don't have identical shapes. + If images in the `ImageCollection` don't have identical shapes. """ - all_images = [img[np.newaxis, ...] for img in self] - try: - ar = np.concatenate(all_images) - except ValueError: - raise ValueError('Image dimensions must agree.') - return ar + return concatenate_images(self) + diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index 6ddae6fe..9dd266bf 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -111,6 +111,12 @@ class TestMultiImage(): 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) + if __name__ == "__main__": run_module_suite() From a130b8d2d9dbe9558552a2e7e0f4fb03f49e8b5b Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 20 Jul 2012 17:57:15 -0400 Subject: [PATCH 061/648] Refactor geometric transforms. --- skimage/transform/__init__.py | 7 +- skimage/transform/_geometric.py | 563 ++++++++++++++++ skimage/transform/_warps.py | 160 +++++ skimage/transform/geometric.py | 764 ---------------------- skimage/transform/tests/test_geometric.py | 171 +---- skimage/transform/tests/test_warps.py | 79 +++ 6 files changed, 830 insertions(+), 914 deletions(-) create mode 100644 skimage/transform/_geometric.py create mode 100644 skimage/transform/_warps.py delete mode 100644 skimage/transform/geometric.py create mode 100644 skimage/transform/tests/test_warps.py diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index b0790992..4721e7d1 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -3,6 +3,7 @@ from .radon_transform import * from .finite_radon_transform import * from ._project import homography as fast_homography from .integral import * -from .geometric import warp, estimate_transformation, geometric_transform, \ - SimilarityTransformation, AffineTransformation, ProjectiveTransformation, \ - PolynomialTransformation, swirl, homography +from ._geometric import (warp, estimate_transform, + SimilarityTransform, AffineTransform, + ProjectiveTransform, PolynomialTransform) +from ._warps import swirl, homography diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py new file mode 100644 index 00000000..0af7ffb1 --- /dev/null +++ b/skimage/transform/_geometric.py @@ -0,0 +1,563 @@ +import math +import numpy as np +from scipy import ndimage +from skimage.util import img_as_float + + +def _stackcopy(a, b): + """Copy b into each color layer of a, such that:: + + a[:,:,0] = a[:,:,1] = ... = b + + Parameters + ---------- + a : (M, N) or (M, N, P) ndarray + Target array. + b : (M, N) + Source array. + + Notes + ----- + Color images are stored as an ``MxNx3`` or ``MxNx4`` arrays. + + """ + if a.ndim == 3: + a[:] = b[:, :, np.newaxis] + else: + a[:] = b + + +class GeometricTransform(object): + """Perform geometric transformations on a set of coordinates. + + Parameters + ---------- + matrix : 3x3 array, optional + Homogeneous transformation matrix. + + """ + def __call__(self, coords): + """Apply forward transformation. + + Parameters + ---------- + coords : Nx2 array + source coordinates + + Returns + ------- + coords : Nx2 array + transformed coordinates + + """ + raise NotImplementedError() + + + def inverse(self, coords): + """Apply inverse transformation. + + Parameters + ---------- + coords : Nx2 array + source coordinates + + Returns + ------- + coords : Nx2 array + transformed coordinates + + """ + raise NotImplementedError() + + + def __add__(self, other): + """Combine this transformation with another. + + """ + raise NotImplementedError() + + +class ProjectiveTransform(GeometricTransform): + """Matrix transformation. + + Apply a projective transformation (homography) on coordinates. + + For each homogeneous coordinate :math:`\mathbf{x} = [x, y, 1]^T`, its + target position is calculated by multiplying with the given matrix, + :math:`H`, to give :math:`H \mathbf{x}`. E.g., to rotate by theta degrees + clockwise, the matrix should be + + :: + + [[cos(theta) -sin(theta) 0] + [sin(theta) cos(theta) 0] + [0 0 1]] + + or, to translate x by 10 and y by 20, + + :: + + [[1 0 10] + [0 1 20] + [0 0 1 ]]. + + """ + + _coefs = range(8) + + def __init__(self, matrix=None): + self._matrix = matrix + + @property + def _inv_matrix(self): + return np.linalg.inv(self._matrix) + + def _apply_mat(self, coords, matrix): + coords = np.array(coords, copy=False, ndmin=2) + + x, y = np.transpose(coords) + src = np.vstack((x, y, np.ones_like(x))) + dst = np.dot(src.transpose(), matrix.transpose()) + + # rescale to homogeneous coordinates + dst[:, 0] /= dst[:, 2] + dst[:, 1] /= dst[:, 2] + + return dst[:, :2] + + def __call__(self, coords): + return self._apply_mat(coords, self._matrix) + + def inverse(self, coords): + return self._apply_mat(coords, self._inv_matrix) + + + def estimate(self, src, dst): + """Set the transformation matrix with the explicit transformation + parameters. + + Parameters + ---------- + src : Nx2 array + source coordinates + dst : Nx2 array + destination coordinates + + """ + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] + rows = src.shape[0] + + #: params: a0, a1, a2, b0, b1, b2, c0, c1 + A = np.zeros((rows * 2, 8)) + A[:rows, 0] = xs + A[:rows, 1] = ys + A[:rows, 2] = 1 + A[:rows, 6] = - xd * xs + A[:rows, 7] = - xd * ys + A[rows:, 3] = xs + A[rows:, 4] = ys + A[rows:, 5] = 1 + A[rows:, 6] = - yd * xs + A[rows:, 7] = - yd * ys + + # Select relevant columns, depending on coeffs + A = A[:, self._coefs] + + b = np.hstack([xd, yd]) + + H = np.zeros((3, 3)) + H.flat[self._coefs] = np.linalg.lstsq(A, b)[0] + H[2, 2] = 1 + + self._matrix = H + + def __add__(self, other): + """Combine this transformation with another. + + """ + if isinstance(other, ProjectiveTransform): + return ProjectiveTransform(np.dot(other._matrix, self._matrix)) + else: + raise TypeError("Cannot combine transformations of differing " + "types.") + + + +class AffineTransform(ProjectiveTransform): + + """2D affine transformation of the form:: + + X = a0*x + a1*y + a2 = + = sx*x*cos(rotation) - sy*y*sin(rotation + shear) + a2 + + Y = b0*x + b1*y + b2 = + = sx*x*sin(rotation) + sy*y*cos(rotation + shear) + b2 + + where ``sx`` and ``sy`` are zoom factors in the x and y directions, + and the homogeneous transformation matrix is:: + + [[a0 a1 a2] + [b0 b1 b2] + [0 0 1]] + + Parameters + ---------- + scale : (sx, sy), floats + Scale factors. + rotation : float + Rotation angle in radians, counter-clockwise direction. + shear : float + Shear angle in radians, counter-clockwise direction. + translation : (tx, ty), floats + Translation in x and y. + + """ + + _coefs = range(6) + + def __init__(self, scale=None, rotation=None, shear=None, translation=None): + ProjectiveTransform.__init__(self) + + if scale is None: + scale = (1, 1) + if rotation is None: + rotation = 0 + if shear is None: + shear = 0 + if translation is None: + translation = (0, 0) + + a = rotation + sx, sy = scale + tx, ty = translation + + self._matrix = np.array([ + [sx * math.cos(a), - sy * math.sin(a + shear), tx], + [sx * math.sin(a), sy * math.cos(a + shear), ty], + [0, 0, 1] + ]) + + +class SimilarityTransform(AffineTransform): + """2D similarity transformation of the form:: + + X = a0*x + b0*y + a1 = + = m*x*cos(rotation) + m*y*sin(rotation) + a1 + + Y = b0*x + a0*y + b1 = + = m*x*sin(rotation) + m*y*cos(rotation) + b1 + + where ``m`` is a zoom factor and the homogeneous transformation matrix is:: + + [[a0 b0 a1] + [b0 a0 b1] + [0 0 1]] + + Parameters + ---------- + scale : float, optional + Scale / zoom factor. + rotation : float, optional + Rotation angle, counter-clockwise, in radians. + translation : (tx, ty) of float + x, y translation parameters + + """ + + def __init__(self, scale=None, rotation=None, translation=None): + if scale is not None: + scale = (scale, scale) + AffineTransform.__init__(self, scale=scale, + rotation=rotation, + shear=0, + translation=translation) + + +class PolynomialTransform(GeometricTransform): + """2D transformation of the form:: + + X = sum[j=0:n]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) + Y = sum[j=0:n]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) + + TODO: Describe structure of coefficients. + Shall we store it as a (2, M) ndarray? + + """ + + def __init__(self, coeffs=None): + """Create polynomial transformation. + + Parameters + ---------- + coeffs : array, optional + polynomial coefficients + + """ + self.coeffs = coeffs + + def estimate(self, src, dst, order): + """Set the transformation matrix with the explicit transformation + parameters. + + Parameters + ---------- + src : Nx2 array + source coordinates + dst : Nx2 array + destination coordinates + order : int + polynomial order (number of coefficients is order + 1) + + """ + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] + rows = src.shape[0] + + # number of unknown polynomial coefficients + u = (order + 1) * (order + 2) + + A = np.zeros((rows * 2, u)) + pidx = 0 + for j in xrange(order + 1): + for i in xrange(j + 1): + A[:rows, pidx] = xs ** (j - i) * ys ** i + A[rows:, pidx + u / 2] = xs ** (j - i) * ys ** i + pidx += 1 + + b = np.hstack([xd, yd]) + + self.coeffs = np.linalg.lstsq(A, b)[0] + + def __call__(self, coords): + """Apply forward transformation. + + Parameters + ---------- + coords : Nx2 array + source coordinates + + Returns + ------- + coords : Nx2 array + transformed coordinates + + """ + x = coords[:, 0] + y = coords[:, 1] + u = len(self.coeffs.ravel()) + # number of coefficients -> u = (order + 1) * (order + 2) + order = int((- 3 + math.sqrt(9 - 4 * (2 - u))) / 2) + dst = np.zeros(coords.shape) + + pidx = 0 + for j in xrange(order + 1): + for i in xrange(j + 1): + dst[:, 0] += self.coeffs[pidx] * x ** (j - i) * y ** i + dst[:, 1] += self.coeffs[pidx + u / 2] * x ** (j - i) * y ** i + pidx += 1 + + return dst + + def inverse(self, coords): + raise Exception( + 'There is no explicit way to do the inverse polynomial ' + 'transformation. Instead, estimate the inverse transformation ' + 'parameters by exchanging source and destination coordinates,' + 'then apply the forward transformation.') + + +TRANSFORMATIONS = { + 'similarity': SimilarityTransform, + 'affine': AffineTransform, + 'projective': ProjectiveTransform, + 'polynomial': PolynomialTransform, +} + + +def estimate_transform(ttype, src, dst, **kwargs): + """Estimate 2D geometric transformation parameters. + + You can determine the over-, well- and under-determined parameters + with the least-squares method. + + Number of source must match number of destination coordinates. + + Parameters + ---------- + ttype : {'similarity', 'affine', 'projective', 'polynomial'} + Type of transform. + kwargs : array or int + Function parameters (src, dst, n, angle):: + + NAME / TTYPE FUNCTION PARAMETERS + 'similarity' `src, `dst` + 'affine' `src, `dst` + 'projective' `src, `dst` + 'polynomial' `src, `dst`, `order` (polynomial order) + + Also see examples below. + + Returns + ------- + tform : :class:`GeometricTransform` + Transform object containing the transformation parameters and providing + access to forward and inverse transformation functions. + + Examples + -------- + >>> import numpy as np + >>> from skimage import transform as tf + + >>> # estimate transformation parameters + >>> src = np.array([0, 0, 10, 10]).reshape((2, 2)) + >>> dst = np.array([12, 14, 1, -20]).reshape((2, 2)) + + >>> tform = tf.estimate_transform('similarity', src, dst) + + >>> tform.inverse(tform.forward(src)) # == src + + >>> # warp image using the estimated transformation + >>> from skimage import data + >>> image = data.camera() + + >>> warp(image, inverse_map=tform.inverse) + + >>> # create transformation with explicit parameters + >>> scale = 1.1 + >>> rotation = 1 + >>> translation = (10, 20) + >>> + >>> tform2 = tf.SimilarityTransform(scale, rotation, translation) + + >>> # unite transformations, applied in order from left to right + >>> tform3 = tform + tform2 + >>> tform3.forward(src) # == tform2.forward(tform.forward(src)) + + """ + ttype = ttype.lower() + if ttype not in TRANSFORMATIONS: + raise ValueError('the transformation type \'%s\' is not' + 'implemented' % ttype) + + tform = TRANSFORMATIONS[ttype]() + tform.estimate(src, dst, **kwargs) + + return tform + + +def matrix_transform(coords, matrix): + """Apply 2D matrix transform. + + Parameters + ---------- + coords : Nx2 array + x, y coordinates to transform + matrix : 3x3 array + Homogeneous transformation matrix. + + Returns + ------- + coords : Nx2 array + transformed coordinates + + """ + return ProjectiveTransform(matrix)(coords) + + +def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, + mode='constant', cval=0., reverse_map=None): + """Warp an image according to a given coordinate transformation. + + Parameters + ---------- + image : 2-D array + Input image. + inverse_map : transformation object, callable xy = f(xy, **kwargs) + Inverse coordinate map. A function that transforms a Px2 array of + ``(x, y)`` coordinates in the *output image* into their corresponding + coordinates in the *source image*. In case of a transformation object + its `inverse` method will be used as transformation function. Also see + examples below. + map_args : dict, optional + Keyword arguments passed to `inverse_map`. + output_shape : tuple (rows, cols) + Shape of the output image generated. + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Examples + -------- + Shift an image to the right: + + >>> from skimage import data + >>> image = data.camera() + >>> + >>> def shift_right(xy): + ... xy[:, 0] -= 10 + ... return xy + >>> + >>> warp(image, shift_right) + + """ + # Backward API compatibility + if reverse_map is not None: + inverse_map = reverse_map + + if image.ndim < 2: + raise ValueError("Input must have more than 1 dimension.") + + image = np.atleast_3d(img_as_float(image)) + ishape = np.array(image.shape) + bands = ishape[2] + + if output_shape is None: + output_shape = ishape + + coords = np.empty(np.r_[3, output_shape], dtype=float) + + ## Construct transformed coordinates + + rows, cols = output_shape[:2] + + # Reshape grid coordinates into a (P, 2) array of (x, y) pairs + tf_coords = np.indices((cols, rows), dtype=float).reshape(2, -1).T + + # Map each (x, y) pair to the source image according to + # the user-provided mapping + if callable(getattr(inverse_map, 'inverse', None)): + inverse_map = inverse_map.inverse + tf_coords = inverse_map(tf_coords, **map_args) + + # Reshape back to a (2, M, N) coordinate grid + tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2) + + # Place the y-coordinate mapping + _stackcopy(coords[1, ...], tf_coords[0, ...]) + + # Place the x-coordinate mapping + _stackcopy(coords[0, ...], tf_coords[1, ...]) + + # colour-coordinate mapping + coords[2, ...] = range(bands) + + # Prefilter not necessary for order 1 interpolation + prefilter = order > 1 + mapped = ndimage.map_coordinates(image, coords, prefilter=prefilter, + mode=mode, order=order, cval=cval) + + # The spline filters sometimes return results outside [0, 1], + # so clip to ensure valid data + return np.clip(mapped.squeeze(), 0, 1) + diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py new file mode 100644 index 00000000..347b8440 --- /dev/null +++ b/skimage/transform/_warps.py @@ -0,0 +1,160 @@ +from ._geometric import warp, ProjectiveTransform +import numpy as np + +def _swirl_mapping(xy, center, rotation, strength, radius): + x, y = xy.T + x0, y0 = center + rho = np.sqrt((x - x0) ** 2 + (y - y0) ** 2) + + # Ensure that the transformation decays to approximately 1/1000-th + # within the specified radius. + radius = radius / 5 * np.log(2) + + theta = rotation + strength * \ + np.exp(-rho / radius) + \ + np.arctan2(y - y0, x - x0) + + xy[..., 0] = x0 + rho * np.cos(theta) + xy[..., 1] = y0 + rho * np.sin(theta) + + return xy + + +def swirl(image, center=None, strength=1, radius=100, rotation=0, + output_shape=None, order=1, mode='constant', cval=0): + """Perform a swirl transformation. + + Parameters + ---------- + image : ndarray + Input image. + center : (x,y) tuple or (2,) ndarray + Center coordinate of transformation. + strength : float + The amount of swirling applied. + radius : float + The extent of the swirl in pixels. The effect dies out + rapidly beyond `radius`. + rotation : float + Additional rotation applied to the image. + + Returns + ------- + swirled : ndarray + Swirled version of the input. + + Other parameters + ---------------- + output_shape : tuple or ndarray + Size of the generated output image. + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + """ + + if center is None: + center = np.array(image.shape)[:2] / 2 + + warp_args = {'center': center, + 'rotation': rotation, + 'strength': strength, + 'radius': radius} + + return warp(image, _swirl_mapping, map_args=warp_args, + output_shape=output_shape, + order=order, mode=mode, cval=cval) + + +def homography(image, H, output_shape=None, order=1, + mode='constant', cval=0.): + """Perform a projective transformation (homography) on an image. + + For each pixel, given its homogeneous coordinate :math:`\mathbf{x} + = [x, y, 1]^T`, its target position is calculated by multiplying + with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`. + E.g., to rotate by theta degrees clockwise, the matrix should be + + :: + + [[cos(theta) -sin(theta) 0] + [sin(theta) cos(theta) 0] + [0 0 1]] + + or, to translate x by 10 and y by 20, + + :: + + [[1 0 10] + [0 1 20] + [0 0 1 ]]. + + Parameters + ---------- + image : 2-D array + Input image. + H : array of shape ``(3, 3)`` + Transformation matrix H that defines the homography. + output_shape : tuple (rows, cols) + Shape of the output image generated. + order : int + Order of splines used in interpolation. + mode : string + How to handle values outside the image borders. Passed as-is + to ndimage. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Examples + -------- + >>> # rotate by 90 degrees around origin and shift down by 2 + >>> x = np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1 + >>> x + array([[1, 2, 3], + [4, 5, 6], + [7, 8, 9]], dtype=uint8) + >>> theta = -np.pi/2 + >>> M = np.array([[np.cos(theta),-np.sin(theta),0], + ... [np.sin(theta), np.cos(theta),2], + ... [0, 0, 1]]) + >>> x90 = homography(x, M, order=1) + >>> x90 + array([[3, 6, 9], + [2, 5, 8], + [1, 4, 7]], dtype=uint8) + >>> # translate right by 2 and down by 1 + >>> y = np.zeros((5,5), dtype=np.uint8) + >>> y[1, 1] = 255 + >>> y + array([[ 0, 0, 0, 0, 0], + [ 0, 255, 0, 0, 0], + [ 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0]], dtype=uint8) + >>> M = np.array([[ 1., 0., 2.], + ... [ 0., 1., 1.], + ... [ 0., 0., 1.]]) + >>> y21 = homography(y, M, order=1) + >>> y21 + array([[ 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0], + [ 0, 0, 0, 255, 0], + [ 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0]], dtype=uint8) + + """ + import warnings + warnings.warn('the homography function is deprecated; ' + 'use the `warp` and `tform` function instead', + category=DeprecationWarning) + + tform = ProjectiveTransform(H) + return warp(image, inverse_map=tform.inverse, output_shape=output_shape, + order=order, mode=mode, cval=cval) diff --git a/skimage/transform/geometric.py b/skimage/transform/geometric.py deleted file mode 100644 index 2051e241..00000000 --- a/skimage/transform/geometric.py +++ /dev/null @@ -1,764 +0,0 @@ -# coding: utf-8 -import math -import numpy as np -from scipy import ndimage -from skimage.util import img_as_float - - -def _stackcopy(a, b): - """Copy b into each color layer of a, such that:: - - a[:,:,0] = a[:,:,1] = ... = b - - Parameters - ---------- - a : (M, N) or (M, N, P) ndarray - Target array. - b : (M, N) - Source array. - - Notes - ----- - Color images are stored as an ``MxNx3`` or ``MxNx4`` arrays. - - """ - if a.ndim == 3: - a[:] = b[:, :, np.newaxis] - else: - a[:] = b - - -def geometric_transform(coords, matrix): - """Apply 2D geometric transformation. - - Parameters - ---------- - ttype : Nx2 array - x, y coordinates to transform - matrix : 3x3 array - homogeneous transformation matrix - - Returns - ------- - coords : Nx2 array - transformed coordinates - """ - coords = np.asarray(coords) - shape = coords.shape - if shape == (2,): - coords = np.array([coords]) - - x, y = np.transpose(coords) - src = np.vstack((x, y, np.ones_like(x))) - dst = np.dot(src.transpose(), matrix.transpose()) - # rescale to homogeneous coordinates - dst[:, 0] /= dst[:, 2] - dst[:, 1] /= dst[:, 2] - - if shape == (2,): - return dst[0, :2] - else: - return dst[:, :2] - - -class GeometricTransformation(object): - - def __init__(self, matrix=None): - """Create geometric transformation which contains the transformation - parameters and can perform forward and reverse transformations. - - Parameters - ---------- - matrix : 3x3 array, optional - homogeneous transformation matrix - - """ - self.matrix = matrix - self.inverse_matrix = None - - def forward(self, coords): - """Apply forward transformation. - - Parameters - ---------- - coords : Nx2 array - source coordinates - - Returns - ------- - coords : Nx2 array - transformed coordinates - - """ - if self.matrix is None: - raise Exception('Transformation matrix must be set or estimated.') - return geometric_transform(coords, self.matrix) - - def reverse(self, coords): - """Apply reverse transformation. - - Parameters - ---------- - coords : Nx2 array - source coordinates - - Returns - ------- - coords : Nx2 array - transformed coordinates - - """ - if self.matrix is None: - raise Exception('Transformation matrix must be set or estimated.') - if self.inverse_matrix is None: - self.inverse_matrix = np.linalg.inv(self.matrix) - return geometric_transform(coords, self.inverse_matrix) - - def __add__(self, other): - if type(self) == type(other): - transformation = self.__class__ - else: - transformation = GeometricTransformation - return transformation(other.matrix.dot(self.matrix)) - - -class SimilarityTransformation(GeometricTransformation): - - """2D similarity transformation of the following form: - X = a0*x - b0*y + a1 = - = m*x*cos(rotation) - m*y*sin(rotation) + a1 - Y = b0*x + a0*y + b1 = - = m*x*sin(rotation) + m*y*cos(rotation) + b1 - where the homogeneous transformation matrix is: - [[a0 -b0 a1] - [b0 a0 b1] - [0 0 1]] - - """ - - def estimate(self, src, dst): - """Set the transformation matrix with the estimated parameters of the - given control points. - - Parameters - ---------- - src : Nx2 array - source coordinates - dst : Nx2 array - destination coordinates - - """ - xs = src[:, 0] - ys = src[:, 1] - xd = dst[:, 0] - yd = dst[:, 1] - rows = src.shape[0] - - #: params: a0, a1, b0, b1 - A = np.zeros((rows * 2, 4)) - A[:rows, 0] = xs - A[:rows, 2] = - ys - A[:rows, 1] = 1 - A[rows:, 2] = xs - A[rows:, 0] = ys - A[rows:, 3] = 1 - - b = np.hstack([xd, yd]) - - a0, a1, b0, b1 = np.linalg.lstsq(A, b)[0] - self.matrix = np.array([[a0, -b0, a1], - [b0, a0, b1], - [ 0, 0, 1]]) - - def from_params(self, scale, rotation, translation): - """Set the transformation matrix with the explicit transformation - parameters. - - Parameters - ---------- - scale : float - scale factor - rotation : float - rotation angle in counter-clockwise direction - translation : (tx, ty) as array, list or tuple - x, y translation parameters - - """ - self.matrix = np.array([ - [math.cos(rotation), - math.sin(rotation), 0], - [math.sin(rotation), math.cos(rotation), 0], - [ 0, 0, 1] - ]) - self.matrix *= scale - self.matrix[0:2, 2] = translation - - @property - def scale(self): - return self.matrix[0, 0] / math.cos(self.rotation) - - @property - def rotation(self): - return math.atan2(self.matrix[1, 0], self.matrix[1, 1]) - - @property - def translation(self): - return self.matrix[0:2, 2] - - -class AffineTransformation(GeometricTransformation): - - """2D affine transformation of the following form - X = a0*x + a1*y + a2 = - = sx*x*cos(rotation) - sy*y*sin(rotation+shear) + a2 - Y = b0*x + b1*y + b2 = - = sx*x*sin(rotation) + sy*y*cos(rotation+shear) + b2 - where the homogeneous transformation matrix is: - [[a0 a1 a2] - [b0 b1 b2] - [0 0 1]] - - """ - - def estimate(self, src, dst): - """Set the transformation matrix with the estimated parameters of the - given control points. - - Parameters - ---------- - src : Nx2 array - source coordinates - dst : Nx2 array - destination coordinates - - """ - xs = src[:, 0] - ys = src[:, 1] - xd = dst[:, 0] - yd = dst[:, 1] - rows = src.shape[0] - - #: params: a0, a1, a2, b0, b1, b2 - A = np.zeros((rows * 2, 6)) - A[:rows, 0] = xs - A[:rows, 1] = ys - A[:rows, 2] = 1 - A[rows:, 3] = xs - A[rows:, 4] = ys - A[rows:, 5] = 1 - - b = np.hstack([xd, yd]) - - a0, a1, a2, b0, b1, b2 = np.linalg.lstsq(A, b)[0] - self.matrix = np.array([[a0, a1, a2], - [b0, b1, b2], - [0, 0, 1]]) - - def from_params(self, scale, rotation, shear, translation): - """Set the transformation matrix with the explicit transformation - parameters. - - Parameters - ---------- - scale : (sx, sy) as array, list or tuple - scale factors - rotation : float - rotation angle in counter-clockwise direction - shear : float - shear angle in counter-clockwise direction - translation : (tx, ty) as array, list or tuple - translation parameters - - """ - sx, sy = scale - self.matrix = 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 - - @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) - return sx, sy - - @property - def rotation(self): - return math.atan2(self.matrix[1, 0], self.matrix[0, 0]) - - @property - def shear(self): - beta = math.atan2(- self.matrix[0, 1], self.matrix[1, 1]) - return beta - self.rotation - - @property - def translation(self): - return self.matrix[0:2, 2] - - -class ProjectiveTransformation(GeometricTransformation): - - """2D projective transformation of the following form - X = (a0 + a1*x + a2*y) / (c0*x + c1*y + 1) - Y = (b0 + b1*x + b2*y) / (c0*x + c1*y + 1) - where the homogeneous transformation matrix is: - [[a0 a1 a2] - [b0 b1 b2] - [c0 c1 1]] - - """ - - def estimate(self, src, dst): - """Set the transformation matrix with the explicit transformation - parameters. - - Parameters - ---------- - src : Nx2 array - source coordinates - dst : Nx2 array - destination coordinates - - """ - xs = src[:, 0] - ys = src[:, 1] - xd = dst[:, 0] - yd = dst[:, 1] - rows = src.shape[0] - - #: params: a0, a1, a2, b0, b1, b2, c0, c1 - A = np.zeros((rows * 2, 8)) - A[:rows, 0] = xs - A[:rows, 1] = ys - A[:rows, 2] = 1 - A[:rows, 6] = - xd * xs - A[:rows, 7] = - xd * ys - A[rows:, 3] = xs - A[rows:, 4] = ys - A[rows:, 5] = 1 - A[rows:, 6] = - yd * xs - A[rows:, 7] = - yd * ys - - b = np.hstack([xd, yd]) - - a0, a1, a2, b0, b1, b2, c0, c1 = np.linalg.lstsq(A, b)[0] - self.matrix = np.array([[a0, a1, a2], - [b0, b1, b2], - [c0, c1, 1]]) - - -class PolynomialTransformation(GeometricTransformation): - - """2D affine transformation of the following form - X = sum[j=0:n]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) - Y = sum[j=0:n]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) - - """ - - def __init__(self, coeffs=None): - """Create polynomial transformation which contains the transformation - parameters and can perform forward and reverse transformations. - - Parameters - ---------- - coeffs : array, optional - polynomial coefficients - - """ - self.coeffs = coeffs - - def estimate(self, src, dst, order): - """Set the transformation matrix with the explicit transformation - parameters. - - Parameters - ---------- - src : Nx2 array - source coordinates - dst : Nx2 array - destination coordinates - order : int - polynomial order (number of coefficients is order + 1) - - """ - xs = src[:, 0] - ys = src[:, 1] - xd = dst[:, 0] - yd = dst[:, 1] - rows = src.shape[0] - - # number of unknown polynomial coefficients - u = (order + 1) * (order + 2) - - A = np.zeros((rows * 2, u)) - pidx = 0 - for j in xrange(order + 1): - for i in xrange(j + 1): - A[:rows, pidx] = xs ** (j - i) * ys ** i - A[rows:, pidx + u / 2] = xs ** (j - i) * ys ** i - pidx += 1 - - b = np.hstack([xd, yd]) - - self.coeffs = np.linalg.lstsq(A, b)[0] - - def forward(self, coords): - """Apply forward transformation. - - Parameters - ---------- - coords : Nx2 array - source coordinates - - Returns - ------- - coords : Nx2 array - transformed coordinates - - """ - x = coords[:, 0] - y = coords[:, 1] - u = len(self.coeffs) - # number of coefficients -> u = (order + 1) * (order + 2) - order = int((- 3 + math.sqrt(9 - 4 * (2 - u))) / 2) - dst = np.zeros(coords.shape) - - pidx = 0 - for j in xrange(order + 1): - for i in xrange(j + 1): - dst[:, 0] += self.coeffs[pidx] * x ** (j - i) * y ** i - dst[:, 1] += self.coeffs[pidx + u / 2] * x ** (j - i) * y ** i - pidx += 1 - - return dst - - def reverse(self, coords): - raise Exception( - 'There is no explicit way to do the reverse polynomial ' - 'transformation. Instead determine the reverse transformation ' - 'parameters by exchanging source and destination coordinates.' - 'Then apply the forward transformation.') - - -TRANSFORMATIONS = { - 'similarity': SimilarityTransformation, - 'affine': AffineTransformation, - 'projective': ProjectiveTransformation, - 'polynomial': PolynomialTransformation, -} - - -def estimate_transformation(ttype, src, dst, order=None): - """Estimate 2D geometric transformation parameters. - - You can determine the over-, well- and under-determined parameters - with the least-squares method. - - Number of source must match number of destination coordinates. - - Parameters - ---------- - ttype : str - one of similarity, affine, projective, polynomial - kwargs :: array or int - function parameters (src, dst, n, angle): - - NAME / TTYPE FUNCTION PARAMETERS - 'similarity' `src, `dst` - 'affine' `src, `dst` - 'projective' `src, `dst` - 'polynomial' `src, `dst`, `order` (polynomial order) - - See examples section below for usage. - - Returns - ------- - tform : subclass of :class:`GeometricTransformation` - tform object containing the transformation parameters and providing - access to forward and reverse transformation functions - - Examples - -------- - >>> import numpy as np - >>> from skimage import transform as tf - >>> # estimate transformation parameters - >>> src = np.array([0, 0, 10, 10]).reshape((2, 2)) - >>> dst = np.array([12, 14, 1, -20]).reshape((2, 2)) - >>> tform = tf.estimate_transformation('similarity', src, dst) - >>> tform.matrix - >>> tform.reverse(tform.forward(src)) # == src - >>> # warp image using the estimated transformation - >>> from skimage import data - >>> image = data.camera() - >>> tf.warp(image, tform) # == warp(image, reverse_map=tform.reverse) - >>> tf.warp(image, reverse_map=tform.forward) - >>> # create transformation with explicit parameters - >>> tform2 = tf.SimilarityTransformation() - >>> scale = 1.1 - >>> rotation = 1 - >>> translation = (10, 20) - >>> tform2.from_params(scale, rotation, translation) - >>> # unite transformations, applied in order from left to right - >>> tform3 = tform + tform2 - >>> tform3.forward(src) # == tform2.forward(tform.forward(src)) - - """ - ttype = ttype.lower() - if ttype not in TRANSFORMATIONS: - raise ValueError('the transformation type \'%s\' is not' - 'implemented' % ttype) - args = [src, dst] - if order is not None and ttype == 'polynomial': - args.append(order) - tform = TRANSFORMATIONS[ttype]() - tform.estimate(*args) - return tform - - -def warp(image, reverse_map=None, map_args={}, output_shape=None, order=1, - mode='constant', cval=0.): - """Warp an image according to a given coordinate transformation. - - Parameters - ---------- - image : 2-D array - Input image. - reverse_map : transformation object, callable xy = f(xy, **kwargs) - Reverse coordinate map. A function that transforms a Px2 array of - ``(x, y)`` coordinates in the *output image* into their corresponding - coordinates in the *source image*. In case of a transformation object - its `reverse` method will be used as transformation function. Also see - examples below. - map_args : dict, optional - Keyword arguments passed to `reverse_map`. - output_shape : tuple (rows, cols) - Shape of the output image generated. - order : int - Order of splines used in interpolation. See - `scipy.ndimage.map_coordinates` for detail. - mode : string - How to handle values outside the image borders. See - `scipy.ndimage.map_coordinates` for detail. - cval : string - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - Examples - -------- - Shift an image to the right: - - >>> from skimage import data - >>> image = data.camera() - >>> - >>> def shift_right(xy): - ... xy[:, 0] -= 10 - ... return xy - >>> - >>> warp(image, shift_right) - - """ - if image.ndim < 2: - raise ValueError("Input must have more than 1 dimension.") - - image = np.atleast_3d(img_as_float(image)) - ishape = np.array(image.shape) - bands = ishape[2] - - if output_shape is None: - output_shape = ishape - - coords = np.empty(np.r_[3, output_shape], dtype=float) - - ## Construct transformed coordinates - - rows, cols = output_shape[:2] - - # Reshape grid coordinates into a (P, 2) array of (x, y) pairs - tf_coords = np.indices((cols, rows), dtype=float).reshape(2, -1).T - - # Map each (x, y) pair to the source image according to - # the user-provided mapping - if callable(getattr(reverse_map, 'reverse', None)): - reverse_map = reverse_map.reverse - tf_coords = reverse_map(tf_coords, **map_args) - - # Reshape back to a (2, M, N) coordinate grid - tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2) - - # Place the y-coordinate mapping - _stackcopy(coords[1, ...], tf_coords[0, ...]) - - # Place the x-coordinate mapping - _stackcopy(coords[0, ...], tf_coords[1, ...]) - - # colour-coordinate mapping - coords[2, ...] = range(bands) - - # Prefilter not necessary for order 1 interpolation - prefilter = order > 1 - mapped = ndimage.map_coordinates(image, coords, prefilter=prefilter, - mode=mode, order=order, cval=cval) - - # The spline filters sometimes return results outside [0, 1], - # so clip to ensure valid data - return np.clip(mapped.squeeze(), 0, 1) - - -def _swirl_mapping(xy, center, rotation, strength, radius): - x, y = xy.T - x0, y0 = center - rho = np.sqrt((x - x0) ** 2 + (y - y0) ** 2) - - # Ensure that the transformation decays to approximately 1/1000-th - # within the specified radius. - radius = radius / 5 * np.log(2) - - theta = rotation + strength * \ - np.exp(-rho / radius) + \ - np.arctan2(y - y0, x - x0) - - xy[..., 0] = x0 + rho * np.cos(theta) - xy[..., 1] = y0 + rho * np.sin(theta) - - return xy - - -def swirl(image, center=None, strength=1, radius=100, rotation=0, - output_shape=None, order=1, mode='constant', cval=0): - """Perform a swirl transformation. - - Parameters - ---------- - image : ndarray - Input image. - center : (x,y) tuple or (2,) ndarray - Center coordinate of transformation. - strength : float - The amount of swirling applied. - radius : float - The extent of the swirl in pixels. The effect dies out - rapidly beyond `radius`. - rotation : float - Additional rotation applied to the image. - - Returns - ------- - swirled : ndarray - Swirled version of the input. - - Other parameters - ---------------- - output_shape : tuple or ndarray - Size of the generated output image. - order : int - Order of splines used in interpolation. See - `scipy.ndimage.map_coordinates` for detail. - mode : string - How to handle values outside the image borders. See - `scipy.ndimage.map_coordinates` for detail. - cval : string - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - """ - - if center is None: - center = np.array(image.shape)[:2] / 2 - - warp_args = {'center': center, - 'rotation': rotation, - 'strength': strength, - 'radius': radius} - - return warp(image, _swirl_mapping, map_args=warp_args, - output_shape=output_shape, - order=order, mode=mode, cval=cval) - - -def homography(image, H, output_shape=None, order=1, - mode='constant', cval=0.): - """Perform a projective transformation (homography) on an image. - - For each pixel, given its homogeneous coordinate :math:`\mathbf{x} - = [x, y, 1]^T`, its target position is calculated by multiplying - with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`. - E.g., to rotate by theta degrees clockwise, the matrix should be - - :: - - [[cos(theta) -sin(theta) 0] - [sin(theta) cos(theta) 0] - [0 0 1]] - - or, to translate x by 10 and y by 20, - - :: - - [[1 0 10] - [0 1 20] - [0 0 1 ]]. - - Parameters - ---------- - image : 2-D array - Input image. - H : array of shape ``(3, 3)`` - Transformation matrix H that defines the homography. - output_shape : tuple (rows, cols) - Shape of the output image generated. - order : int - Order of splines used in interpolation. - mode : string - How to handle values outside the image borders. Passed as-is - to ndimage. - cval : string - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - Examples - -------- - >>> # rotate by 90 degrees around origin and shift down by 2 - >>> x = np.arange(9, dtype=np.uint8).reshape((3, 3)) + 1 - >>> x - array([[1, 2, 3], - [4, 5, 6], - [7, 8, 9]], dtype=uint8) - >>> theta = -np.pi/2 - >>> M = np.array([[np.cos(theta),-np.sin(theta),0], - ... [np.sin(theta), np.cos(theta),2], - ... [0, 0, 1]]) - >>> x90 = homography(x, M, order=1) - >>> x90 - array([[3, 6, 9], - [2, 5, 8], - [1, 4, 7]], dtype=uint8) - >>> # translate right by 2 and down by 1 - >>> y = np.zeros((5,5), dtype=np.uint8) - >>> y[1, 1] = 255 - >>> y - array([[ 0, 0, 0, 0, 0], - [ 0, 255, 0, 0, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0]], dtype=uint8) - >>> M = np.array([[ 1., 0., 2.], - ... [ 0., 1., 1.], - ... [ 0., 0., 1.]]) - >>> y21 = homography(y, M, order=1) - >>> y21 - array([[ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 255, 0], - [ 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0]], dtype=uint8) - - """ - import warnings - warnings.warn('the homography function is deprecated; ' - 'use the `warp` and `tform` function instead', - category=DeprecationWarning) - - tform = ProjectiveTransformation(H) - return warp(image, reverse_map=tform.reverse, output_shape=output_shape, - order=order, mode=mode, cval=cval) diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index 1648e6f1..4430673c 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -1,12 +1,10 @@ import numpy as np from numpy.testing import assert_array_almost_equal -from skimage.transform.geometric import _stackcopy -from skimage.transform import estimate_transformation, homography, warp, \ - fast_homography, SimilarityTransformation, AffineTransformation, \ - ProjectiveTransformation, PolynomialTransformation -from skimage import transform as tf, data, img_as_float -from skimage.color import rgb2gray +from skimage.transform._geometric import _stackcopy +from skimage.transform import (estimate_transform, SimilarityTransform, + AffineTransform, ProjectiveTransform, + PolynomialTransform) SRC = np.array([ @@ -42,171 +40,50 @@ def test_stackcopy(): def test_similarity_estimation(): #: exact solution - tform = estimate_transformation('similarity', SRC[:2, :], DST[:2, :]) - assert_array_almost_equal(tform.forward(SRC[:2, :]), DST[:2, :]) - assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) + tform = estimate_transform('similarity', SRC[:2, :], DST[:2, :]) + assert_array_almost_equal(tform(SRC[:2, :]), DST[:2, :]) + assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) #: over-determined - tform = estimate_transformation('similarity', SRC, DST) - ref = np.array( - [[2.3632898110e+02, -5.5876792257e+00, 2.5331569391e+03], - [5.5876792257e+00, 2.3632898110e+02, 2.4358232635e+03], - [0.0000000000e+00, 0.0000000000e+00, 1.0000000000e+00]]) - assert_array_almost_equal(tform.matrix, ref) - assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) + tform = estimate_transform('similarity', SRC, DST) + assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) -def test_similarity_explicit(): - tform = SimilarityTransformation() - scale = 0.1 - rotation = 1 - translation = (1, 1) - tform.from_params(scale, rotation, 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 - tform = estimate_transformation('affine', SRC[:3, :], DST[:3, :]) - assert_array_almost_equal(tform.forward(SRC[:3, :]), DST[:3, :]) - assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) + tform = estimate_transform('affine', SRC[:3, :], DST[:3, :]) + assert_array_almost_equal(tform(SRC[:3, :]), DST[:3, :]) + assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) #: over-determined - tform = estimate_transformation('affine', SRC, DST) - ref = np.array( - [[2.2573930047e+02, 7.1588596765e+00, 2.5126622012e+03], - [2.1234856855e+01, 2.4931019555e+02, 2.4143862183e+03], - [0.0000000000e+00, 0.0000000000e+00, 1.0000000000e+00]]) - assert_array_almost_equal(tform.matrix, ref) - assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) - - -def test_affine_explicit(): - tform = AffineTransformation() - scale = (0.1, 0.13) - rotation = 1 - shear = 0.1 - translation = (1, 1) - tform.from_params(scale, rotation, shear, translation) - assert_array_almost_equal(tform.scale, scale) - assert_array_almost_equal(tform.rotation, rotation) - assert_array_almost_equal(tform.shear, shear) - assert_array_almost_equal(tform.translation, translation) + tform = estimate_transform('affine', SRC, DST) + assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) def test_projective(): #: exact solution - tform = estimate_transformation('projective', SRC[:4, :], DST[:4, :]) - ref = np.array( - [[ 1.9466901291e+02, -1.1888183994e+01, 2.2832379309e+03], - [ -8.6910077540e+00, 2.2162069773e+02, 2.2211673699e+03], - [ -1.2695966735e-02, -9.6053624285e-03, 1.0000000000e+00]]) - assert_array_almost_equal(tform.matrix, ref, 6) - assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) + tform = estimate_transform('projective', SRC[:4, :], DST[:4, :]) + assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) #: over-determined - tform = estimate_transformation('projective', SRC[:4, :], DST[:4, :]) - ref = np.array( - [[ 1.9466901291e+02, -1.1888183994e+01, 2.2832379309e+03], - [ -8.6910077540e+00, 2.2162069773e+02, 2.2211673699e+03], - [ -1.2695966735e-02, -9.6053624285e-03, 1.0000000000e+00]]) - assert_array_almost_equal(tform.matrix, ref, 6) - assert_array_almost_equal(tform.reverse(tform.forward(SRC)), SRC) + tform = estimate_transform('projective', SRC[:4, :], DST[:4, :]) + assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) def test_polynomial(): - tform = estimate_transformation('polynomial', SRC, DST, order=10) - assert_array_almost_equal(tform.forward(SRC), DST, 6) + tform = estimate_transform('polynomial', SRC, DST, order=10) + assert_array_almost_equal(tform(SRC), DST, 6) def test_union(): - tform1 = SimilarityTransformation() - scale1 = 0.1 - rotation1 = 1 - translation1 = (0, 0) - tform1.from_params(scale1, rotation1, translation1) - - tform2 = SimilarityTransformation() - scale2 = 0.1 - rotation2 = 1 - translation2 = (0, 0) - tform2.from_params(scale2, rotation2, translation2) + tform1 = SimilarityTransform(1, 0.3) + tform2 = SimilarityTransform(1, 0.6) + tform3 = SimilarityTransform(1, 0.9) tform = tform1 + tform2 - assert_array_almost_equal(tform.scale, scale1 * scale2) - assert_array_almost_equal(tform.rotation, rotation1 + rotation2) - - -def test_warp(): - x = np.zeros((5, 5), dtype=np.uint8) - x[2, 2] = 255 - x = img_as_float(x) - theta = -np.pi/2 - tform = SimilarityTransformation() - tform.from_params(1, theta, (0, 4)) - - x90 = warp(x, tform, order=1) - assert_array_almost_equal(x90, np.rot90(x)) - - x90 = warp(x, tform.reverse, order=1) - assert_array_almost_equal(x90, np.rot90(x)) - - -def test_homography(): - x = np.zeros((5, 5), dtype=np.uint8) - x[1, 1] = 255 - x = img_as_float(x) - theta = -np.pi/2 - M = np.array([[np.cos(theta),-np.sin(theta),0], - [np.sin(theta), np.cos(theta),4], - [0, 0, 1]]) - x90 = homography(x, M, order=1) - assert_array_almost_equal(x90, np.rot90(x)) - - -def test_fast_homography(): - img = rgb2gray(data.lena()).astype(np.uint8) - img = img[:, :100] - - theta = np.deg2rad(30) - scale = 0.5 - tx, ty = 50, 50 - - H = np.eye(3) - S = scale * np.sin(theta) - C = scale * np.cos(theta) - - H[:2, :2] = [[C, -S], [S, C]] - H[:2, 2] = [tx, ty] - - for mode in ('constant', 'mirror', 'wrap'): - p0 = homography(img, H, mode=mode, order=1) - p1 = fast_homography(img, H, mode=mode) - p1 = np.round(p1) - - ## import matplotlib.pyplot as plt - ## f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4) - ## ax0.imshow(img) - ## ax1.imshow(p0, cmap=plt.cm.gray) - ## ax2.imshow(p1, cmap=plt.cm.gray) - ## ax3.imshow(np.abs(p0 - p1), cmap=plt.cm.gray) - ## plt.show() - - d = np.mean(np.abs(p0 - p1)) - assert d < 0.2 - - -def test_swirl(): - image = img_as_float(data.checkerboard()) - - swirl_params = {'radius': 80, 'rotation': 0, 'order': 2, 'mode': 'reflect'} - swirled = tf.swirl(image, strength=10, **swirl_params) - unswirled = tf.swirl(swirled, strength=-10, **swirl_params) - - assert np.mean(np.abs(image - unswirled)) < 0.01 + assert_array_almost_equal(tform._matrix, tform3._matrix) if __name__ == "__main__": diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py new file mode 100644 index 00000000..047b477b --- /dev/null +++ b/skimage/transform/tests/test_warps.py @@ -0,0 +1,79 @@ +from numpy.testing import assert_array_almost_equal, run_module_suite +import numpy as np + +from skimage.transform import (warp, homography, fast_homography, + SimilarityTransform) +from skimage import transform as tf, data, img_as_float +from skimage.color import rgb2gray + + +def test_warp(): + x = np.zeros((5, 5), dtype=np.uint8) + x[2, 2] = 255 + x = img_as_float(x) + theta = -np.pi/2 + tform = SimilarityTransform(1, theta, (0, 4)) + + x90 = warp(x, tform, order=1) + assert_array_almost_equal(x90, np.rot90(x)) + + x90 = warp(x, tform.inverse, order=1) + assert_array_almost_equal(x90, np.rot90(x)) + + +def test_homography(): + x = np.zeros((5, 5), dtype=np.uint8) + x[1, 1] = 255 + x = img_as_float(x) + theta = -np.pi/2 + M = np.array([[np.cos(theta),-np.sin(theta),0], + [np.sin(theta), np.cos(theta),4], + [0, 0, 1]]) + x90 = homography(x, M, order=1) + assert_array_almost_equal(x90, np.rot90(x)) + + +def test_fast_homography(): + img = rgb2gray(data.lena()).astype(np.uint8) + img = img[:, :100] + + theta = np.deg2rad(30) + scale = 0.5 + tx, ty = 50, 50 + + H = np.eye(3) + S = scale * np.sin(theta) + C = scale * np.cos(theta) + + H[:2, :2] = [[C, -S], [S, C]] + H[:2, 2] = [tx, ty] + + for mode in ('constant', 'mirror', 'wrap'): + p0 = homography(img, H, mode=mode, order=1) + p1 = fast_homography(img, H, mode=mode) + p1 = np.round(p1) + + ## import matplotlib.pyplot as plt + ## f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4) + ## ax0.imshow(img) + ## ax1.imshow(p0, cmap=plt.cm.gray) + ## ax2.imshow(p1, cmap=plt.cm.gray) + ## ax3.imshow(np.abs(p0 - p1), cmap=plt.cm.gray) + ## plt.show() + + d = np.mean(np.abs(p0 - p1)) + assert d < 0.2 + + +def test_swirl(): + image = img_as_float(data.checkerboard()) + + swirl_params = {'radius': 80, 'rotation': 0, 'order': 2, 'mode': 'reflect'} + swirled = tf.swirl(image, strength=10, **swirl_params) + unswirled = tf.swirl(swirled, strength=-10, **swirl_params) + + assert np.mean(np.abs(image - unswirled)) < 0.01 + + +if __name__ == "__main__": + run_module_suite() From 46f6733fc9f108638c2a6dc536c1f633b7eb0190 Mon Sep 17 00:00:00 2001 From: Dharhas Pothina Date: Fri, 20 Jul 2012 17:00:36 -0500 Subject: [PATCH 062/648] added xyz2lab, lab2xyz not quite working --- skimage/color/colorconv.py | 149 ++++++++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 5e2fa5e1..7f8687db 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -44,7 +44,9 @@ References from __future__ import division __all__ = ['convert_colorspace', 'rgb2hsv', 'hsv2rgb', 'rgb2xyz', 'xyz2rgb', - 'rgb2rgbcie', 'rgbcie2rgb', 'rgb2grey', 'rgb2gray', 'gray2rgb'] + 'rgb2rgbcie', 'rgbcie2rgb', 'rgb2grey', 'rgb2gray', 'gray2rgb', + 'xyz2lab', 'lab2xyz', + ] __docformat__ = "restructuredtext en" @@ -543,3 +545,148 @@ def gray2rgb(image): M, N = image.shape return np.dstack((image, image, image)) + + +#---------------------- +# Constants for CIE LAB +#---------------------- +_one_third = 1.0 / 3.0 +_sixteen_hundred_sixteenth = 16.0 / 116.0 +# Observer= 2A, Illuminant= D65 +_xref = 0.95047 +_yref = 1.0 +_zref = 1.08883 +_inv_xref = 1.0 / _xref +_inv_yref = 1.0 / _yref +_inv_zref = 1.0 / _zref + +#-------------------------------------------------------------- +# The conversion functions that make use of the constants above +#-------------------------------------------------------------- + +def xyz2lab(xyz): + """XYZ to CIE-LAB color space conversion. + + Parameters + ---------- + xyz : array_like + The image in XYZ format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in CIE-LAB format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `xyz` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + Observer= 2A, Illuminant= D65 + CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 + + References + ---------- + .. [1] http://www.easyrgb.com/index.php?X=MATH&H=07#text7 + .. [2] http://en.wikipedia.org/wiki/Lab_color_space + + Examples + -------- + >>> import os + >>> from skimage import data_dir + >>> from skimage.color import rgb2xyz, xyz2lab + >>> from skimage.io import imread + >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena_xyz = rgb2xyz(lena) + >>> lena_lab = xyz2lab(lena_xyz) + """ + arr = _prepare_colorarray(xyz) + out = np.empty_like(arr) + + # scale by CIE XYZ tristimulus values of the reference white point + x, y, z = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] + x *= _inv_xref + y *= _inv_yref + z *= _inv_zref + + # Nonlinear distortion and linear transformation + mask = arr > 0.008856 + arr[mask] = np.power(arr[mask], _one_third) + arr[~mask] = 7.787 * arr[~mask] + _sixteen_hundred_sixteenth + + # Vector scaling + L = (116. * y) - 16. + a = 500.0 * (x - y) + b = 200.0 * (y - z) + + # -- output + out[:, :, 0] = L + out[:, :, 1] = a + out[:, :, 2] = b + + # remove NaN + out[np.isnan(out)] = 0 + + return out + +def lab2xyz(lab): + """CIE-LAB to XYZcolor space conversion. + + Parameters + ---------- + lab : array_like + 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). + + Raises + ------ + ValueError + If `lab` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + Observer= 2A, Illuminant= D65 + CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 + + References + ---------- + .. [1] http://www.easyrgb.com/index.php?X=MATH&H=07#text7 + .. [2] http://en.wikipedia.org/wiki/Lab_color_space + + """ + + arr = _prepare_colorarray(lab) + out = np.empty_like(arr) + + L, a, b = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] + y = (L + 16.) / 116. + x = a / 500. + y + z = y - b / 200. + + out[:, :, 0] = x + out[:, :, 1] = y + out[:, :, 2] = z + + out_cube = np.power(out,3) + mask = out > 0.206893 + out[mask] = np.power(out[mask], 3.) + out[~mask] = (out[~mask] - _sixteen_hundred_sixteenth) / 7.787 + + # rescale Observer= 2 deg, Illuminant= D65 + #x, y, z = out[:, :, 0], out[:, :, 1], out[:, :, 2] + out[:, :, 0] *= _xref + out[:, :, 1] *= _yref + out[:, :, 2] *= _zref + + # remove NaN + out[np.isnan(out)] = 0 + + return out + + From 3b7ab0dd9e0296b7a457cdf6c4be701eec42ca81 Mon Sep 17 00:00:00 2001 From: Leon Tietz Date: Fri, 20 Jul 2012 17:19:12 -0500 Subject: [PATCH 063/648] corrected plot title --- doc/examples/plot_hough_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_hough_transform.py b/doc/examples/plot_hough_transform.py index 51a90c64..5416d662 100644 --- a/doc/examples/plot_hough_transform.py +++ b/doc/examples/plot_hough_transform.py @@ -109,7 +109,7 @@ plt.title('Input image') plt.subplot(132) plt.imshow(edges, cmap=plt.cm.gray) -plt.title('Sobel edges') +plt.title('Canny edges') plt.subplot(133) plt.imshow(edges * 0) From bb1add8abd66ca68fb867dafe7ba45b26d7fac78 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Fri, 20 Jul 2012 18:18:05 -0400 Subject: [PATCH 064/648] BUG: Ignore colorconv RuntimeWarning:invalid value encountered in true_divide. Sometimes zero divided by zero can occur in this code. Saturation was already explicitly set to zero when 'delta' is zero. According to Wikipedia, hue is undefined when 'delta' here is zero, so explicitly set to zero. --- skimage/color/colorconv.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 5e2fa5e1..5a158a4c 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -164,8 +164,10 @@ def rgb2hsv(rgb): # -- S channel delta = arr.ptp(-1) + # Ignore warning for zero divided by zero + old_settings = np.seterr(invalid='ignore') out_s = delta / out_v - out_s[delta == 0] = 0 + out_s[delta == 0.] = 0. # -- H channel # red is max @@ -180,6 +182,9 @@ def rgb2hsv(rgb): idx = (arr[:, :, 2] == out_v) out[idx, 0] = 4. + (arr[idx, 0] - arr[idx, 1]) / delta[idx] out_h = (out[:, :, 0] / 6.) % 1. + out_h[delta == 0.] = 0. + + np.seterr(**old_settings) # -- output out[:, :, 0] = out_h From 9006c1dab34b424104d9a2aba47891a5e7457e8d Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 20 Jul 2012 17:38:06 -0500 Subject: [PATCH 065/648] Sort files from a global pattern alphanumerically Users usually expect an alphanumeric sort, not lexicographic sort, on their filenames. This is now the behaviour of ImageCollection. --- skimage/io/collection.py | 35 +++++++++++++++++++++++++++-- skimage/io/tests/test_collection.py | 16 +++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index e698b52b..c754c8ad 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -5,11 +5,42 @@ from __future__ import with_statement __all__ = ['MultiImage', 'ImageCollection', 'imread'] from glob import glob +import re import numpy as np from ._io import imread +def _tryint(s): + try: + return int(s) + except ValueError: + return s + +def alphanumeric_key(s): + """Convert string to list of strings and ints that gives intuitive sorting. + + Parameters + ---------- + s: string + + Returns + ------- + k: a list of strings and ints + + Examples + -------- + >>> alphanumeric_key('z23a') + ['z', 23, 'a'] + >>> filenames = ['f9.10.png', 'f9.9.png', 'f10.10.png', 'f10.9.png'] + >>> sorted(filenames) + ['f10.10.png', 'f10.9.png', 'f9.10.png', 'f9.9.png'] + >>> sorted(filenames, key=alphanumeric_key) + ['f9.9.png', 'f9.10.png', 'f10.9.png', 'f10.10.png'] + """ + k = [_tryint(c) for c in re.split('([0-9]+)', s)] + return k + class MultiImage(object): """A class containing a single multi-frame image. @@ -213,7 +244,7 @@ class ImageCollection(object): (128, 128, 3) >>> ic = io.ImageCollection('/tmp/work/*.png:/tmp/other/*.jpg') - + """ def __init__(self, load_pattern, conserve_memory=True, load_func=None): """Load and manage a collection of images.""" @@ -222,7 +253,7 @@ class ImageCollection(object): self._files = [] for pattern in load_pattern: self._files.extend(glob(pattern)) - self._files.sort() + self._files = sorted(self._files, key=alphanumeric_key) else: self._files = load_pattern diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index 0d420ae7..969d3e56 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -7,6 +7,7 @@ from numpy.testing.decorators import skipif from skimage import data_dir from skimage.io import ImageCollection, MultiImage +from skimage.io.collection import alphanumeric_key try: @@ -19,6 +20,21 @@ else: if sys.version_info[0] > 2: basestring = str +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'] + self.sorted_filenames = \ + ['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) + class TestImageCollection(): pattern = [os.path.join(data_dir, pic) for pic in ['camera.png', From 6b591e27a0b2542340c4d2f0f41b1de5d305f3a2 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 17:49:28 -0500 Subject: [PATCH 066/648] Add PlotPlugin and cleanup code. --- skimage/viewer/plugins/base.py | 136 +++++++++++++++++++++++++++++--- skimage/viewer/plugins/canny.py | 2 +- skimage/viewer/viewers/core.py | 47 ++++++++++- 3 files changed, 171 insertions(+), 14 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 9a8c2cda..d1ef3eec 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,7 +1,31 @@ from PyQt4 import QtGui + +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg from skimage.io._plugins.q_color_mixer import IntelligentSlider +class PlotCanvas(FigureCanvasQTAgg): + """Canvas for displaying images. + + This canvas derives from Matplotlib, and has attributes `fig` and `ax`, + which point to Matplotlib figure and axes. + """ + def __init__(self, parent, height, width, **kwargs): + print height, width + self.fig, self.ax = plt.subplots(figsize=(height, width), **kwargs) + + FigureCanvasQTAgg.__init__(self, self.fig) + FigureCanvasQTAgg.setSizePolicy(self, + QtGui.QSizePolicy.Expanding, + QtGui.QSizePolicy.Expanding) + FigureCanvasQTAgg.updateGeometry(self) + # Note: `setParent` must be called after `FigureCanvasQTAgg.__init__`. + self.setParent(parent) + + class Plugin(QtGui.QDialog): """Base class for widgets that interact with the axes. @@ -12,24 +36,22 @@ class Plugin(QtGui.QDialog): useblit : bool If True, use blitting to speed up animation. Only available on some backends. If None, set to True when using Agg backend, otherwise False. - figure : :class:`~matplotlib.figure.Figure` - If None, create a figure with a single axes. - no_toolbar : bool - If True, figure created by plugin has no toolbar. This has no effect - on figures passed into `Plugin`. Attributes ---------- - viewer : ImageViewer + image_viewer : ImageViewer Window containing image used in measurement. image : array Image used in measurement/manipulation. overlay : array Image used in measurement/manipulation. """ - def __init__(self, image_viewer, callback=None, height=100, width=400): - self._viewer = image_viewer + def __init__(self, image_viewer, callback=None, height=100, width=400, + useblit=None): + self.image_viewer = image_viewer QtGui.QDialog.__init__(self, image_viewer) + self.image_viewer.plugins.append(self) + self.setWindowTitle('Image Plugin') self.layout = QtGui.QGridLayout(self) self.resize(width, height) @@ -40,8 +62,17 @@ class Plugin(QtGui.QDialog): self.arguments = [image_viewer.original_image] self.keyword_arguments= {} - self.overlay = self._viewer.overlay - self.image = self._viewer.image + self.overlay = self.image_viewer.overlay + self.image = self.image_viewer.image + + + if useblit is None: + useblit = True if mpl.backends.backend.endswith('Agg') else False + self.useblit = useblit + self.cids = [] + self.artists = [] + + self.connect_event('draw_event', self.on_draw) def caller(self, *args): arguments = [self._get_value(a) for a in self.arguments] @@ -69,3 +100,88 @@ class Plugin(QtGui.QDialog): self.layout.addWidget(slider, self.row, 0) self.row += 1 return name.replace(' ', '_'), slider + + def redraw(self): + self.canvas.draw_idle() + + def closeEvent(self, event): + """Disconnect all artists and events from ImageViewer. + + Note that events must be connected using `self.connect_event` and + artists must be appended to `self.artists`. + """ + self.disconnect_image_events() + self.remove_artists() + self.image_viewer.plugins.remove(self) + self.image_viewer.redraw() + self.close() + + def connect_event(self, event, callback): + """Connect callback with an event. + + This should be used in lieu of `figure.canvas.mpl_connect` since this + function stores call back ids for later clean up. + """ + cid = self.image_viewer.connect_event(event, callback) + self.cids.append(cid) + + def disconnect_image_events(self): + """Disconnect all events created by this widget.""" + for c in self.cids: + self.image_viewer.disconnect_event(c) + + def remove_artists(self): + """Disconnect artists that are connected to the *image plot*.""" + for a in self.artists: + self.image_viewer.remove_artist(a) + + +class PlotPlugin(Plugin): + """Plugin for ImageViewer that contains a plot Canvas. + + Parameters + ---------- + image_viewer : ImageViewer instance. + Window containing image used in measurement/manipulation. + figure : :class:`~matplotlib.figure.Figure` + If None, create a figure with a single axes. + useblit : bool + If True, use blitting to speed up animation. Only available on some + backends. If None, set to True when using Agg backend, otherwise False. + + Attributes + ---------- + image_viewer : ImageViewer + Window containing image used in measurement. + image : array + Image used in measurement/manipulation. + overlay : array + Image used in measurement/manipulation. + """ + def __init__(self, image_viewer, useblit=None, **kwargs): + Plugin.__init__(self, image_viewer, **kwargs) + # Add plot for displaying intensity profile. + self.add_plot() + self.connect_event('draw_event', self.on_draw) + + def on_draw(self, event): + """Save image background when blitting. + + The saved image is used to "clear" the figure before redrawing artists. + """ + if self.useblit: + bbox = self.image_viewer.ax.bbox + self.img_background = self.image_viewer.canvas.copy_from_bbox(bbox) + + def add_plot(self, height=4, width=4): + self.canvas = PlotCanvas(self, height, width) + self.fig = self.canvas.fig + #TODO: Converted color is slightly different than Qt background. + qpalette = QtGui.QPalette() + qcolor = qpalette.color(QtGui.QPalette.Window) + bgcolor = qcolor.toRgb().value() + if np.isscalar(bgcolor): + bgcolor = str(bgcolor / 255.) + self.fig.patch.set_facecolor(bgcolor) + self.ax = self.canvas.ax + self.layout.addWidget(self.canvas, self.row, 0) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index db702fc9..176dcf0d 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -17,4 +17,4 @@ class CannyPlugin(Plugin): def callback(self, *args, **kwargs): image = canny(*args, **kwargs) - self._viewer.overlay = image + self.image_viewer.overlay = image diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index e1c4d003..76eb6070 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -53,6 +53,17 @@ class ImageViewer(QtGui.QMainWindow): self.canvas = ImageCanvas(self.main_widget, image) self.fig = self.canvas.fig self.ax = self.canvas.ax + self.ax.autoscale(enable=False) + self.image_plot = self.ax.images[0] + self.plugins = [] + + # List of axes artists to check for removal. + self._axes_artists = [self.ax.artists, + self.ax.collections, + self.ax.images, + self.ax.lines, + self.ax.patches, + self.ax.texts] self.layout = QtGui.QVBoxLayout(self.main_widget) self.layout.addWidget(self.canvas) @@ -61,6 +72,8 @@ class ImageViewer(QtGui.QMainWindow): # self.statusBar().showMessage("coordinates") self.original_image = image self.image = image + + self.overlay_plot = None self._overlay = None @property @@ -80,10 +93,10 @@ class ImageViewer(QtGui.QMainWindow): @overlay.setter def overlay(self, image): self._overlay = image - if len(self.ax.images) == 1: - self.ax.imshow(image, cmap=self.overlay_cmap) + if self.overlay_plot is None: + self.overlay_plot = self.ax.imshow(image, cmap=self.overlay_cmap) else: - self.ax.images[1].set_array(image) + self.overlay_plot.set_array(image) self.canvas.draw_idle() def closeEvent(self, ce): @@ -93,4 +106,32 @@ class ImageViewer(QtGui.QMainWindow): super(ImageViewer, self).show() sys.exit(qApp.exec_()) + @property + def climits(self): + return self.image_plot.get_clim() + + @climits.setter + def climits(self, limits): + cmin, cmax = limits + self.image_plot.set_clim(vmin=cmin, vmax=cmax) + + def connect_event(self, event, callback): + cid = self.canvas.mpl_connect(event, callback) + return cid + + def disconnect_event(self, callback_id): + self.canvas.mpl_disconnect(callback_id) + + def add_artist(self, artist): + self.ax.add_artist(artist) + + def remove_artist(self, artist): + """Disconnect all artists created by this widget.""" + # There's probably a smarter way to do this. + for artist_list in self._axes_artists: + if artist in artist_list: + artist_list.remove(artist) + + def redraw(self): + self.canvas.draw_idle() From 4b53c92c141f73d7fedd2555db28fe98b3149c31 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Fri, 20 Jul 2012 16:34:07 -0400 Subject: [PATCH 067/648] BUG: Fix OTSU thresholding tests with matplotlib IO plugin. The matplotlib IO returns float arrays from [0, 1], which gives difference results than a ubyte array. Explicitly convert to a ubyte array in the tests. --- skimage/filter/tests/test_thresholding.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 59d7e431..8278fa6f 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -73,11 +73,13 @@ class TestSimpleImage(): def test_otsu_camera_image(): - assert threshold_otsu(data.camera()) == 87 + camera = skimage.img_as_ubyte(data.camera()) + assert threshold_otsu(camera) == 87 def test_otsu_coins_image(): - assert threshold_otsu(data.coins()) == 107 + coins = skimage.img_as_ubyte(data.coins()) + assert threshold_otsu(coins) == 107 def test_otsu_coins_image_as_float(): @@ -86,7 +88,8 @@ def test_otsu_coins_image_as_float(): def test_otsu_lena_image(): - assert threshold_otsu(data.lena()) == 141 + lena = skimage.img_as_ubyte(data.lena()) + assert threshold_otsu(lena) == 141 if __name__ == '__main__': From 699b5d92697d3c6bb0ef17882d0c9714fb6d7dee Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 20 Jul 2012 17:50:04 -0500 Subject: [PATCH 068/648] Add test coverage for alphabetic sort alphanumeric_key should sort filenames correctly when they differ in text, not just numbers. --- skimage/io/tests/test_collection.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index 969d3e56..c8ba154a 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -24,9 +24,11 @@ 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'] + self.filenames = ['f9.10.png', 'f9.9.png', 'f10.10.png', 'f10.9.png', + 'e9.png', 'e10.png', 'em.png'] self.sorted_filenames = \ - ['f9.9.png', 'f9.10.png', 'f10.9.png', 'f10.10.png'] + ['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) From afd33afc5e6112ec12ed34ec3bca40de4311b753 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 17:52:01 -0500 Subject: [PATCH 069/648] Add LineProfile plugin. --- skimage/viewer/plugins/lineprofile.py | 239 +++++++++++++++++++++++++ viewer_examples/plugins/lineprofile.py | 10 ++ 2 files changed, 249 insertions(+) create mode 100644 skimage/viewer/plugins/lineprofile.py create mode 100644 viewer_examples/plugins/lineprofile.py diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py new file mode 100644 index 00000000..3839a6e0 --- /dev/null +++ b/skimage/viewer/plugins/lineprofile.py @@ -0,0 +1,239 @@ +import numpy as np +import scipy.ndimage as ndi +from skimage.util.dtype import dtype_range + +from .base import PlotPlugin + + +__all__ = ['LineProfile'] + + +class LineProfile(PlotPlugin): + """Plugin to compute interpolated intensity under a scan line on an image. + + Parameters + ---------- + image_viewer : ImageViewer instance. + Window containing image used in measurement. + useblit : bool + If True, use blitting to speed up animation. Only available on some + backends. If None, set to True when using Agg backend, otherwise False. + linewidth : float + Line width for interpolation. Wider lines average over more pixels. + epsilon : float + Maximum pixel distance allowed when selecting end point of scan line. + limits : tuple or {None, 'image', 'dtype'} + (minimum, maximum) intensity limits for plotted profile. The following + special values are defined: + + None : rescale based on min/max intensity along selected scan line. + 'image' : fixed scale based on min/max intensity in image. + 'dtype' : fixed scale based on min/max intensity of image dtype. + """ + + def __init__(self, image_viewer, useblit=None, + linewidth=1, epsilon=5, limits='image'): + super(LineProfile, self).__init__(image_viewer, height=200, width=600, + useblit=useblit) + + self.linewidth = linewidth + self.epsilon = epsilon + + if limits == 'image': + self.limits = (np.min(self.image), np.max(self.image)) + elif limits == 'dtype': + self.limits = dtype_range[self.image.dtype.type] + elif limits is None or len(limits) == 2: + self.limits = limits + else: + raise ValueError("Unrecognized `limits`: %s" % limits) + + if not limits is None: + self.ax.set_ylim(self.limits) + + h, w = self.image.shape + + self._init_end_pts = np.array([[w/3, h/2], [2*w/3, h/2]]) + self.end_pts = self._init_end_pts.copy() + + x, y = np.transpose(self.end_pts) + self.scan_line = self.image_viewer.ax.plot(x, y, 'y-s', markersize=5, + lw=linewidth, alpha=0.5, + solid_capstyle='butt')[0] + self.artists.append(self.scan_line) + + scan_data = profile_line(self.image, self.end_pts) + self.profile = self.ax.plot(scan_data, 'k-')[0] + self._autoscale_view() + + self._active_pt = None + + self.connect_event('key_press_event', self.on_key_press) + self.connect_event('button_press_event', self.on_mouse_press) + self.connect_event('button_release_event', self.on_mouse_release) + self.connect_event('motion_notify_event', self.on_move) + self.connect_event('scroll_event', self.on_scroll) + + self.image_viewer.redraw() + print self.help() + + def help(self): + helpstr = ("Line profile tool", + "+ and - keys or mouse scroll changes width of scan line.", + "Select and drag ends of the scan line to adjust it.") + return '\n'.join(helpstr) + + def get_profile(self): + """Return intensity profile of the selected line. + + Returns + ------- + end_pts: (2, 2) array + The positions ((x1, y1), (x2, y2)) of the line ends. + profile: 1d array + Profile of intensity values. + """ + end_pts = self.scan_line.get_xydata() + profile = self.profile.get_ydata() + return end_pts, profile + + def on_scroll(self, event): + if not event.inaxes: return + if event.button == 'up': + self._thicken_scan_line() + elif event.button == 'down': + self._shrink_scan_line() + + def on_key_press(self, event): + if not event.inaxes: return + elif event.key == '+': + self._thicken_scan_line() + elif event.key == '-': + self._shrink_scan_line() + elif event.key == 'r': + self.reset() + + def _thicken_scan_line(self): + self.linewidth += 1 + self.line_changed(None, None) + + def _shrink_scan_line(self): + if self.linewidth > 1: + self.linewidth -= 1 + self.line_changed(None, None) + + def _autoscale_view(self): + if self.limits is None: + self.ax.autoscale_view(tight=True) + else: + self.ax.autoscale_view(scaley=False, tight=True) + + def get_pt_under_cursor(self, event): + """Return index of the end point under cursor, if sufficiently close""" + xy = np.asarray(self.scan_line.get_xydata()) + xyt = self.scan_line.get_transform().transform(xy) + xt, yt = xyt[:, 0], xyt[:, 1] + d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2) + indseq = np.nonzero(np.equal(d, np.amin(d)))[0] + ind = indseq[0] + if d[ind] >= self.epsilon: + ind = None + return ind + + def on_mouse_press(self, event): + if event.button != 1: return + if event.inaxes==None: return + self._active_pt = self.get_pt_under_cursor(event) + + def on_mouse_release(self, event): + if event.button != 1: return + self._active_pt = None + + def on_move(self, event): + if event.button != 1: return + if self._active_pt is None: return + if not self.image_viewer.ax.in_axes(event): return + x,y = event.xdata, event.ydata + self.line_changed(x, y) + + def reset(self): + self.end_pts = self._init_end_pts.copy() + self.scan_line.set_data(np.transpose(self.end_pts)) + self.line_changed(None, None) + + def line_changed(self, x, y): + if x is not None: + self.end_pts[self._active_pt, :] = x, y + self.scan_line.set_data(np.transpose(self.end_pts)) + self.scan_line.set_linewidth(self.linewidth) + + scan = profile_line(self.image, self.end_pts, linewidth=self.linewidth) + self.profile.set_xdata(np.arange(scan.shape[0])) + self.profile.set_ydata(scan) + + self.ax.relim() + + if self.useblit: + self.image_viewer.canvas.restore_region(self.img_background) + self.ax.draw_artist(self.scan_line) + self.ax.draw_artist(self.profile) + self.image_viewer.canvas.blit(self.image_viewer.ax.bbox) + + self._autoscale_view() + + self.image_viewer.redraw() + self.redraw() + + +def profile_line(img, end_pts, linewidth=1): + """Return the intensity profile of an image measured along a scan line. + + Parameters + ---------- + img : 2d array + The image. + end_pts: (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_pts + x1, y1 = point1 = np.asarray(point1, dtype = float) + x2, y2 = point2 = np.asarray(point2, dtype = float) + dx, dy = point2 - point1 + + # Quick calculation if perfectly horizontal or vertical (remove?) + if x1 == x2: + pixels = img[min(y1, y2) : max(y1, y2)+1, + x1 - linewidth / 2 : x1 + linewidth / 2 + 1] + intensities = pixels.mean(axis = 1) + return intensities + elif y1 == y2: + pixels = img[y1 - linewidth / 2 : y1 + linewidth / 2 + 1, + min(x1, x2) : max(x1, x2)+1] + intensities = pixels.mean(axis = 0) + return intensities + + theta = np.arctan2(dy,dx) + a = dy/dx + b = y1 - a * x1 + length = np.hypot(dx, dy) + + line_x = np.linspace(min(x1, x2), max(x1, x2), 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]) + pixels = ndi.map_coordinates(img, perp_lines) + intensities = pixels.mean(axis=1) + + return intensities diff --git a/viewer_examples/plugins/lineprofile.py b/viewer_examples/plugins/lineprofile.py new file mode 100644 index 00000000..cecb2539 --- /dev/null +++ b/viewer_examples/plugins/lineprofile.py @@ -0,0 +1,10 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.lineprofile import LineProfile + + +image = data.camera() +viewer = ImageViewer(image) +p = LineProfile(viewer) +p.show() +viewer.show() From 67eb914ef6d8468ed7333c3ab31017be7457dd83 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Fri, 20 Jul 2012 19:09:05 -0400 Subject: [PATCH 070/648] TST: Add buffer for threshold tests. In case conversion from float to integer is imprecise. --- skimage/filter/tests/test_thresholding.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 8278fa6f..97d3d9e3 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -74,12 +74,12 @@ class TestSimpleImage(): def test_otsu_camera_image(): camera = skimage.img_as_ubyte(data.camera()) - assert threshold_otsu(camera) == 87 + assert 86 < threshold_otsu(camera) < 88 def test_otsu_coins_image(): coins = skimage.img_as_ubyte(data.coins()) - assert threshold_otsu(coins) == 107 + assert 106 < threshold_otsu(coins) < 108 def test_otsu_coins_image_as_float(): @@ -89,7 +89,7 @@ def test_otsu_coins_image_as_float(): def test_otsu_lena_image(): lena = skimage.img_as_ubyte(data.lena()) - assert threshold_otsu(lena) == 141 + assert 140 < threshold_otsu(lena) < 142 if __name__ == '__main__': From 47d5f028e50488ccef552ecc4c3906df12d9d70b Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 18:19:37 -0500 Subject: [PATCH 071/648] Fix: Move on_draw method to base Plugin --- skimage/viewer/plugins/base.py | 24 +++++++++++++----------- skimage/viewer/plugins/lineprofile.py | 1 + 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index d1ef3eec..98217ca8 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -46,6 +46,8 @@ class Plugin(QtGui.QDialog): overlay : array Image used in measurement/manipulation. """ + draws_on_image = False + def __init__(self, image_viewer, callback=None, height=100, width=400, useblit=None): self.image_viewer = image_viewer @@ -72,7 +74,17 @@ class Plugin(QtGui.QDialog): self.cids = [] self.artists = [] - self.connect_event('draw_event', self.on_draw) + if self.draws_on_image: + self.connect_event('draw_event', self.on_draw) + + def on_draw(self, event): + """Save image background when blitting. + + The saved image is used to "clear" the figure before redrawing artists. + """ + if self.useblit: + bbox = self.image_viewer.ax.bbox + self.img_background = self.image_viewer.canvas.copy_from_bbox(bbox) def caller(self, *args): arguments = [self._get_value(a) for a in self.arguments] @@ -162,16 +174,6 @@ class PlotPlugin(Plugin): Plugin.__init__(self, image_viewer, **kwargs) # Add plot for displaying intensity profile. self.add_plot() - self.connect_event('draw_event', self.on_draw) - - def on_draw(self, event): - """Save image background when blitting. - - The saved image is used to "clear" the figure before redrawing artists. - """ - if self.useblit: - bbox = self.image_viewer.ax.bbox - self.img_background = self.image_viewer.canvas.copy_from_bbox(bbox) def add_plot(self, height=4, width=4): self.canvas = PlotCanvas(self, height, width) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 3839a6e0..90fa07b6 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -30,6 +30,7 @@ class LineProfile(PlotPlugin): 'image' : fixed scale based on min/max intensity in image. 'dtype' : fixed scale based on min/max intensity of image dtype. """ + draws_on_image = True def __init__(self, image_viewer, useblit=None, linewidth=1, epsilon=5, limits='image'): From fedad0d82675b9ca61a1aab0f883efb475960942 Mon Sep 17 00:00:00 2001 From: Dharhas Pothina Date: Fri, 20 Jul 2012 18:45:56 -0500 Subject: [PATCH 072/648] fixed array being modified in place. lab2xyz working --- skimage/color/colorconv.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 7f8687db..352e3039 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -553,13 +553,15 @@ def gray2rgb(image): _one_third = 1.0 / 3.0 _sixteen_hundred_sixteenth = 16.0 / 116.0 # Observer= 2A, Illuminant= D65 -_xref = 0.95047 -_yref = 1.0 -_zref = 1.08883 +_xref = 0.95047 +_yref = 1. +_zref = 1.08883 _inv_xref = 1.0 / _xref _inv_yref = 1.0 / _yref _inv_zref = 1.0 / _zref + + #-------------------------------------------------------------- # The conversion functions that make use of the constants above #-------------------------------------------------------------- @@ -602,7 +604,7 @@ def xyz2lab(xyz): >>> lena_xyz = rgb2xyz(lena) >>> lena_lab = xyz2lab(lena_xyz) """ - arr = _prepare_colorarray(xyz) + arr = _prepare_colorarray(xyz).copy() out = np.empty_like(arr) # scale by CIE XYZ tristimulus values of the reference white point @@ -661,22 +663,21 @@ def lab2xyz(lab): """ - arr = _prepare_colorarray(lab) + arr = _prepare_colorarray(lab).copy() out = np.empty_like(arr) L, a, b = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] y = (L + 16.) / 116. - x = a / 500. + y - z = y - b / 200. + x = (a / 500.) + y + z = y - (b / 200.) out[:, :, 0] = x out[:, :, 1] = y out[:, :, 2] = z - out_cube = np.power(out,3) - mask = out > 0.206893 + mask = out > 0.2068966 out[mask] = np.power(out[mask], 3.) - out[~mask] = (out[~mask] - _sixteen_hundred_sixteenth) / 7.787 + out[~mask] = (out[~mask] - _sixteen_hundred_sixteenth) / 7.787*1000 # rescale Observer= 2 deg, Illuminant= D65 #x, y, z = out[:, :, 0], out[:, :, 1], out[:, :, 2] From 74cde286b405a06480efba046242e79f92b20882 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 20:33:25 -0500 Subject: [PATCH 073/648] allow qt_plugin imsave() function to write to file-like objects --- skimage/io/_plugins/qt_plugin.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/skimage/io/_plugins/qt_plugin.py b/skimage/io/_plugins/qt_plugin.py index dd4e4a8f..401589a9 100644 --- a/skimage/io/_plugins/qt_plugin.py +++ b/skimage/io/_plugins/qt_plugin.py @@ -7,8 +7,8 @@ import sys window_manager.acquire('qt') try: - from PyQt4.QtGui import (QApplication, QMainWindow, QImage, QPixmap, - QLabel, QWidget) + from PyQt4.QtGui import (QApplication, QImage, + QLabel, QMainWindow, QPixmap, QWidget) from PyQt4 import QtCore, QtGui import sip import warnings @@ -148,12 +148,21 @@ def _app_show(): print 'No images to show. See `imshow`.' -def imsave(filename, img): +def imsave(filename, img, format_str=None): # we can add support for other than 3D uint8 here... img = prepare_for_display(img) qimg = QImage(img.data, img.shape[1], img.shape[0], img.strides[0], QImage.Format_RGB888) - saved = qimg.save(filename) + if _is_filelike(filename): + byte_array = QtCore.QByteArray() + qbuffer = QtCore.QBuffer() + qbuffer.open(QtCore.QIODevice.ReadWrite) + saved = qimg.save(qbuffer, format_str.upper()) + qbuffer.seek(0) + filename.write(qbuffer.readAll()) + qbuffer.close() + else: + saved = qimg.save(filename) if not saved: from textwrap import dedent msg = dedent( @@ -161,3 +170,7 @@ def imsave(filename, img): for the QT imsave plugin are: BMP, JPG, JPEG, PNG, PPM, TIFF, XBM, XPM''') raise RuntimeError(msg) + + +def _is_filelike(possible_filelike): + return callable(getattr(possible_filelike, 'write', None)) From fbb2ec3afab89c0d3866edcae7f636df32bdd9bd Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 20:34:12 -0500 Subject: [PATCH 074/648] close temp StringIO buffers when we're done with them --- skimage/io/_io.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index c271f25b..514b828c 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -59,12 +59,16 @@ class Image(np.ndarray): def _repr_png_(self): str_buffer = StringIO.StringIO() imsave(str_buffer, self, format_str='png') - return str_buffer.getvalue() + return_str = str_buffer.getvalue() + str_buffer.close() + return return_str def _repr_jpeg_(self): str_buffer = StringIO.StringIO() imsave(str_buffer, self, format_str='jpeg') - return str_buffer.getvalue() + return_str = str_buffer.getvalue() + str_buffer.close() + return return_str def __setstate__(self, state): nd_state, subclass_state = state From 9cbf2ef3694522547cb8c5e0cb1b2393b54c6691 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sat, 21 Jul 2012 00:51:55 -0500 Subject: [PATCH 075/648] Simplify alphanumeric_key logic Thanks to Tony Yu for the suggestion. --- skimage/io/collection.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index c754c8ad..35e4e877 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -11,12 +11,6 @@ import numpy as np from ._io import imread -def _tryint(s): - try: - return int(s) - except ValueError: - return s - def alphanumeric_key(s): """Convert string to list of strings and ints that gives intuitive sorting. @@ -34,11 +28,11 @@ def alphanumeric_key(s): ['z', 23, 'a'] >>> filenames = ['f9.10.png', 'f9.9.png', 'f10.10.png', 'f10.9.png'] >>> sorted(filenames) - ['f10.10.png', 'f10.9.png', 'f9.10.png', 'f9.9.png'] + ['f10.10.png', 'f10.9.png', 'f9.10.png', 'f9.9.png', 'e10.png'] >>> sorted(filenames, key=alphanumeric_key) - ['f9.9.png', 'f9.10.png', 'f10.9.png', 'f10.10.png'] + ['e10.png', 'f9.9.png', 'f9.10.png', 'f10.9.png', 'f10.10.png'] """ - k = [_tryint(c) for c in re.split('([0-9]+)', s)] + k = [int(c) if c.isdigit() else c for c in re.split('([0-9]+)', s)] return k class MultiImage(object): From 8cd912a89467835be5a79d6b1620d16c0563d17d Mon Sep 17 00:00:00 2001 From: Dharhas Pothina Date: Sat, 21 Jul 2012 09:10:05 -0500 Subject: [PATCH 076/648] tests added for xyz2lab and lab2xyz --- skimage/color/tests/test_colorconv.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index cfe81c58..7be4f454 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -23,7 +23,8 @@ from skimage.color import ( rgb2xyz, xyz2rgb, rgb2rgbcie, rgbcie2rgb, convert_colorspace, - rgb2grey, gray2rgb + rgb2grey, gray2rgb, + xyz2lab, lab2xyz, ) from skimage import data_dir @@ -43,6 +44,20 @@ class TestColorconv(TestCase): colbars_point75 = colbars * 0.75 colbars_point75_array = np.swapaxes(colbars_point75.reshape(3, 4, 2), 0, 2) + xyz_array = np.array([[[0.4124, 0.21260, 0.01930]], #red + [[0, 0, 0]], #black + [[.9505, 1., 1.089]], #white + [[.1805, .0722, .9505]], #blue + [[.07719, .15438, .02573]], #green + ]) + + 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 + ]) + # RGB to HSV def test_rgb2hsv_conversion(self): rgb = img_as_float(self.img_rgb)[::16, ::16] @@ -149,6 +164,13 @@ class TestColorconv(TestCase): def test_rgb2grey_on_grey(self): rgb2grey(np.random.random((5, 5))) + # test matrices for xyz2lab and lab2xyz generated using http://www.easyrgb.com/index.php?X=CALC + # Note: easyrgb website displays xyz*100 + def test_xyz2lab(self): + assert_array_almost_equal(xyz2lab(self.xyz_array), self.lab_array, decimal=3) + + def test_lab2xyz(self): + assert_array_almost_equal(lab2xyz(self.lab_array), self.xyz_array, decimal=3) def test_gray2rgb(): x = np.array([0, 0.5, 1]) From dcb7dacc6c5f37e9ab534a7e289044db4c77c010 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Sat, 21 Jul 2012 10:23:20 -0500 Subject: [PATCH 077/648] open buffer on on byte_array --- skimage/io/_plugins/qt_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/_plugins/qt_plugin.py b/skimage/io/_plugins/qt_plugin.py index 401589a9..bb4cf232 100644 --- a/skimage/io/_plugins/qt_plugin.py +++ b/skimage/io/_plugins/qt_plugin.py @@ -155,7 +155,7 @@ def imsave(filename, img, format_str=None): img.strides[0], QImage.Format_RGB888) if _is_filelike(filename): byte_array = QtCore.QByteArray() - qbuffer = QtCore.QBuffer() + qbuffer = QtCore.QBuffer(byte_array) qbuffer.open(QtCore.QIODevice.ReadWrite) saved = qimg.save(qbuffer, format_str.upper()) qbuffer.seek(0) From fb3f201a2a657ca282110dbdbd1196a611cc9246 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 17:08:19 -0500 Subject: [PATCH 078/648] Clean up old code and add docstrings. --- skimage/viewer/plugins/base.py | 7 +++-- skimage/viewer/viewers/core.py | 55 ++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 98217ca8..4d2f39c1 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -113,9 +113,6 @@ class Plugin(QtGui.QDialog): self.row += 1 return name.replace(' ', '_'), slider - def redraw(self): - self.canvas.draw_idle() - def closeEvent(self, event): """Disconnect all artists and events from ImageViewer. @@ -175,6 +172,10 @@ class PlotPlugin(Plugin): # Add plot for displaying intensity profile. self.add_plot() + def redraw(self): + """Redraw plot.""" + self.canvas.draw_idle() + def add_plot(self, height=4, width=4): self.canvas = PlotCanvas(self, height, width) self.fig = self.canvas.fig diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 76eb6070..a2bc2385 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -27,6 +27,29 @@ class ImageCanvas(FigureCanvasQTAgg): class ImageViewer(QtGui.QMainWindow): + """Viewer for displaying images. + + This viewer is a simple container object that holds a Matplotlib axes + for showing images. `ImageViewer` doesn't subclass the Matplotlib axes (or + figure) because of the high probability of name collisions. + + Parameters + ---------- + image : array + Image being viewed. + + Attributes + ---------- + canvas, fig, ax : Matplotlib canvas, figure, and axes + Matplotlib canvas, figure, and axes used to display image. + image : array + Image being viewed. Setting this value will update the displayed frame. + original_image : array + Plugins typically operate on (but don't change) the original image. + plugins : list + List of attached plugins. + """ + def __init__(self, image): # Start main loop @@ -54,7 +77,11 @@ class ImageViewer(QtGui.QMainWindow): self.fig = self.canvas.fig self.ax = self.canvas.ax self.ax.autoscale(enable=False) - self.image_plot = self.ax.images[0] + + self._image_plot = self.ax.images[0] + self._overlay_plot = None + self._overlay = None + self.plugins = [] # List of axes artists to check for removal. @@ -73,9 +100,6 @@ class ImageViewer(QtGui.QMainWindow): self.original_image = image self.image = image - self.overlay_plot = None - self._overlay = None - @property def image(self): return self._img @@ -83,7 +107,7 @@ class ImageViewer(QtGui.QMainWindow): @image.setter def image(self, image): self._img = image - self.ax.images[0].set_array(image) + self._image_plot.set_array(image) self.canvas.draw_idle() @property @@ -93,10 +117,10 @@ class ImageViewer(QtGui.QMainWindow): @overlay.setter def overlay(self, image): self._overlay = image - if self.overlay_plot is None: - self.overlay_plot = self.ax.imshow(image, cmap=self.overlay_cmap) + if self._overlay_plot is None: + self._overlay_plot = self.ax.imshow(image, cmap=self.overlay_cmap) else: - self.overlay_plot.set_array(image) + self._overlay_plot.set_array(image) self.canvas.draw_idle() def closeEvent(self, ce): @@ -106,27 +130,21 @@ class ImageViewer(QtGui.QMainWindow): super(ImageViewer, self).show() sys.exit(qApp.exec_()) - @property - def climits(self): - return self.image_plot.get_clim() - - @climits.setter - def climits(self, limits): - cmin, cmax = limits - self.image_plot.set_clim(vmin=cmin, vmax=cmax) - def connect_event(self, event, callback): + """Connect callback function to matplotlib event and return id.""" cid = self.canvas.mpl_connect(event, callback) return cid def disconnect_event(self, callback_id): + """Disconnect callback by its id (returned by `connect_event`).""" self.canvas.mpl_disconnect(callback_id) def add_artist(self, artist): + """Add matplotlib artist to image viewer.""" self.ax.add_artist(artist) def remove_artist(self, artist): - """Disconnect all artists created by this widget.""" + """Disconnect matplotlib artist from image viewer.""" # There's probably a smarter way to do this. for artist_list in self._axes_artists: if artist in artist_list: @@ -134,4 +152,3 @@ class ImageViewer(QtGui.QMainWindow): def redraw(self): self.canvas.draw_idle() - From bd3ee7830675bf68722e0f7ed117a62efea90cb1 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 17:09:29 -0500 Subject: [PATCH 079/648] Delete overlay when deleting plugin. --- skimage/viewer/plugins/canny.py | 4 ++++ skimage/viewer/viewers/core.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 176dcf0d..81cc07fe 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -18,3 +18,7 @@ class CannyPlugin(Plugin): def callback(self, *args, **kwargs): image = canny(*args, **kwargs) self.image_viewer.overlay = image + + def closeEvent(self, event): + self.image_viewer.overlay = None + super(CannyPlugin, self).closeEvent(event) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index a2bc2385..65939e9a 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -117,7 +117,10 @@ class ImageViewer(QtGui.QMainWindow): @overlay.setter def overlay(self, image): self._overlay = image - if self._overlay_plot is None: + if image is None: + self.ax.images.remove(self._overlay_plot) + self._overlay_plot = None + elif self._overlay_plot is None: self._overlay_plot = self.ax.imshow(image, cmap=self.overlay_cmap) else: self._overlay_plot.set_array(image) From 9f0449e663d468899372ee1ddac04707e978a7f5 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 18:11:32 -0500 Subject: [PATCH 080/648] Add docstring for `overlay` and reorder methods. --- skimage/viewer/viewers/core.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 65939e9a..c3e8235f 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -46,6 +46,9 @@ class ImageViewer(QtGui.QMainWindow): Image being viewed. Setting this value will update the displayed frame. original_image : array Plugins typically operate on (but don't change) the original image. + overlay : array + Overlay displayed on top of image. This overlay defaults to a color map + with alpha values varying linearly from 0 to 1. plugins : list List of attached plugins. """ @@ -100,6 +103,16 @@ class ImageViewer(QtGui.QMainWindow): self.original_image = image self.image = image + def closeEvent(self, ce): + self.close() + + def show(self): + super(ImageViewer, self).show() + sys.exit(qApp.exec_()) + + def redraw(self): + self.canvas.draw_idle() + @property def image(self): return self._img @@ -108,7 +121,7 @@ class ImageViewer(QtGui.QMainWindow): def image(self, image): self._img = image self._image_plot.set_array(image) - self.canvas.draw_idle() + self.redraw() @property def overlay(self): @@ -126,13 +139,6 @@ class ImageViewer(QtGui.QMainWindow): self._overlay_plot.set_array(image) self.canvas.draw_idle() - def closeEvent(self, ce): - self.close() - - def show(self): - super(ImageViewer, self).show() - sys.exit(qApp.exec_()) - def connect_event(self, event, callback): """Connect callback function to matplotlib event and return id.""" cid = self.canvas.mpl_connect(event, callback) @@ -152,6 +158,3 @@ class ImageViewer(QtGui.QMainWindow): for artist_list in self._axes_artists: if artist in artist_list: artist_list.remove(artist) - - def redraw(self): - self.canvas.draw_idle() From c21fe1c2f9219dee5a425f1d3db4e3221e7b09be Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 18:55:38 -0500 Subject: [PATCH 081/648] Show coordinate and intensity info in status bar. --- skimage/viewer/viewers/core.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index c3e8235f..1f77915d 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -52,8 +52,6 @@ class ImageViewer(QtGui.QMainWindow): plugins : list List of attached plugins. """ - - def __init__(self, image): # Start main loop global qApp @@ -85,6 +83,8 @@ class ImageViewer(QtGui.QMainWindow): self._overlay_plot = None self._overlay = None + self.original_image = image + self.image = image self.plugins = [] # List of axes artists to check for removal. @@ -98,10 +98,14 @@ class ImageViewer(QtGui.QMainWindow): self.layout = QtGui.QVBoxLayout(self.main_widget) self.layout.addWidget(self.canvas) - #TODO: Add coordinate display - # self.statusBar().showMessage("coordinates") - self.original_image = image - self.image = image + #TODO: Set status bar to fixed-width font so status doesn't wiggle + status_bar = self.statusBar() + self.status_message = status_bar.showMessage + sb_size = status_bar.sizeHint() + cs_size = self.canvas.sizeHint() + self.resize(cs_size.width(), cs_size.height() + sb_size.height()) + + self.connect_event('motion_notify_event', self.update_status_bar) def closeEvent(self, ce): self.close() @@ -158,3 +162,18 @@ class ImageViewer(QtGui.QMainWindow): for artist_list in self._axes_artists: if artist in artist_list: artist_list.remove(artist) + + def update_status_bar(self, event): + if event.inaxes and event.inaxes.get_navigate(): + self.status_message(self._format_coord(event.xdata, event.ydata)) + else: + self.status_message('') + + def _format_coord(self, x, y): + # callback function to format coordinate display in status bar + x = int(x + 0.5) + y = int(y + 0.5) + try: + return "%4s @ [%4s, %4s]" % (self.image[y, x], x, y) + except IndexError: + return "" From 221cf733d1bf1d6f40f942d4b6496e6cfa7d7131 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 19:48:26 -0500 Subject: [PATCH 082/648] Add plugin names --- skimage/viewer/plugins/base.py | 4 ++-- skimage/viewer/plugins/canny.py | 2 ++ skimage/viewer/plugins/lineprofile.py | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 4d2f39c1..aa7229ad 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -46,6 +46,7 @@ class Plugin(QtGui.QDialog): overlay : array Image used in measurement/manipulation. """ + name = 'Plugin' draws_on_image = False def __init__(self, image_viewer, callback=None, height=100, width=400, @@ -54,7 +55,7 @@ class Plugin(QtGui.QDialog): QtGui.QDialog.__init__(self, image_viewer) self.image_viewer.plugins.append(self) - self.setWindowTitle('Image Plugin') + self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) self.resize(width, height) self.row = 0 @@ -67,7 +68,6 @@ class Plugin(QtGui.QDialog): self.overlay = self.image_viewer.overlay self.image = self.image_viewer.image - if useblit is None: useblit = True if mpl.backends.backend.endswith('Agg') else False self.useblit = useblit diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 81cc07fe..88312036 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -4,6 +4,8 @@ from skimage.filter import canny class CannyPlugin(Plugin): + name = 'Canny Filter' + def __init__(self, image_viewer, *args, **kwargs): height = kwargs.get('height', 100) width = kwargs.get('width', 400) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 90fa07b6..485839d0 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -30,6 +30,7 @@ class LineProfile(PlotPlugin): 'image' : fixed scale based on min/max intensity in image. 'dtype' : fixed scale based on min/max intensity of image dtype. """ + name = 'Line Profile' draws_on_image = True def __init__(self, image_viewer, useblit=None, From 4739165b8cda941f8e775eabe31fb6223aed5729 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 20:08:47 -0500 Subject: [PATCH 083/648] Add `update_on` parameter to slider and allow update on release. --- skimage/io/_plugins/q_color_mixer.py | 16 ++++++++++++---- skimage/viewer/plugins/base.py | 12 ++++++------ skimage/viewer/plugins/canny.py | 9 ++++++--- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/skimage/io/_plugins/q_color_mixer.py b/skimage/io/_plugins/q_color_mixer.py index 085bd751..7a5d1778 100644 --- a/skimage/io/_plugins/q_color_mixer.py +++ b/skimage/io/_plugins/q_color_mixer.py @@ -6,7 +6,6 @@ from PyQt4.QtCore import Qt from util import ColorMixer - class IntelligentSlider(QWidget): ''' A slider that adds a 'name' attribute and calls a callback with 'name' as an argument to the registered callback. @@ -19,7 +18,8 @@ class IntelligentSlider(QWidget): The range of the slider is hardcoded from zero - 1000, but it supports a conversion factor so you can scale the results''' - def __init__(self, name, a, b, callback, orientation='vertical'): + def __init__(self, name, a, b, callback, orientation='vertical', + update_on='move'): QWidget.__init__(self) self.name = name self.callback = callback @@ -41,7 +41,12 @@ class IntelligentSlider(QWidget): self.slider = QSlider(orientation_slider) self.slider.setRange(0, 1000) self.slider.setValue(500) - self.slider.valueChanged.connect(self.slider_changed) + if update_on == 'move': + self.slider.sliderMoved.connect(self.slider_changed) + elif update_on == 'release': + self.slider.sliderReleased.connect(self.slider_changed) + else: + raise ValueError("Unexpected value %s for 'update_on'" % update_on) self.name_label = QLabel() self.name_label.setText(self.name) @@ -62,9 +67,12 @@ class IntelligentSlider(QWidget): self.layout.addWidget(self.name_label, 0, 0) self.layout.addWidget(self.slider, 0, 1, alignment) self.layout.addWidget(self.value_label, 0, 2) + else: + msg = "Unexpected value %s for 'orientation'" + raise ValueError(msg % orientation) # bind this to the valueChanged signal of the slider - def slider_changed(self, val): + def slider_changed(self): val = self.val() self.value_label.setText(str(val)[:4]) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index aa7229ad..1757f257 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -98,17 +98,17 @@ class Plugin(QtGui.QDialog): else: return param - def add_argument(self, name, low, high, callback): - name, slider = self.add_slider(name, low, high, callback) + def add_argument(self, name, low, high, callback, **kwargs): + name, slider = self.add_slider(name, low, high, callback, **kwargs) self.arguments[name] = slider - def add_keyword_argument(self, name, low, high, callback): - name, slider = self.add_slider(name, low, high, callback) + def add_keyword_argument(self, name, low, high, callback, **kwargs): + name, slider = self.add_slider(name, low, high, callback, **kwargs) self.keyword_arguments[name] = slider - def add_slider(self, name, low, high, callback): + def add_slider(self, name, low, high, callback, **kwargs): slider = IntelligentSlider(name, low, high, callback, - orientation='horizontal') + orientation='horizontal', **kwargs) self.layout.addWidget(slider, self.row, 0) self.row += 1 return name.replace(' ', '_'), slider diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 88312036..cf3b5253 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -11,9 +11,12 @@ class CannyPlugin(Plugin): width = kwargs.get('width', 400) super(CannyPlugin, self).__init__(image_viewer, width=width, height=height) - self.add_keyword_argument('sigma', 0.005, 0, self.caller) - self.add_keyword_argument('low_threshold', 0.255, 0, self.caller) - self.add_keyword_argument('high_threshold', 0.255, 0, self.caller) + self.add_keyword_argument('sigma', 0.005, 0, self.caller, + update_on='release') + self.add_keyword_argument('low_threshold', 0.255, 0, self.caller, + update_on='release') + self.add_keyword_argument('high_threshold', 0.255, 0, self.caller, + update_on='release') # Call callback so that image is updated to slider values. self.caller() From 48ac757ab8303709cdeebb7903440f564664bd60 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 20:29:43 -0500 Subject: [PATCH 084/648] Refactor image overlays to special plugin base class. --- skimage/viewer/plugins/base.py | 53 ++++++++++++++++++++++++++------- skimage/viewer/plugins/canny.py | 8 ++--- skimage/viewer/viewers/core.py | 25 +--------------- 3 files changed, 47 insertions(+), 39 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 1757f257..eb138295 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -6,6 +6,8 @@ import matplotlib.pyplot as plt from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg from skimage.io._plugins.q_color_mixer import IntelligentSlider +from skimage.viewer.utils import clear_red + class PlotCanvas(FigureCanvasQTAgg): """Canvas for displaying images. @@ -33,6 +35,11 @@ class Plugin(QtGui.QDialog): ---------- image_viewer : ImageViewer instance. Window containing image used in measurement/manipulation. + callback : function + Function that gets called to update ImageViewer. Alternatively, this + can also be defined as a method in a Plugin subclass. + height, width : int + Size of plugin window in pixels. useblit : bool If True, use blitting to speed up animation. Only available on some backends. If None, set to True when using Agg backend, otherwise False. @@ -43,8 +50,6 @@ class Plugin(QtGui.QDialog): Window containing image used in measurement. image : array Image used in measurement/manipulation. - overlay : array - Image used in measurement/manipulation. """ name = 'Plugin' draws_on_image = False @@ -65,7 +70,6 @@ class Plugin(QtGui.QDialog): self.arguments = [image_viewer.original_image] self.keyword_arguments= {} - self.overlay = self.image_viewer.overlay self.image = self.image_viewer.image if useblit is None: @@ -152,11 +156,6 @@ class PlotPlugin(Plugin): ---------- image_viewer : ImageViewer instance. Window containing image used in measurement/manipulation. - figure : :class:`~matplotlib.figure.Figure` - If None, create a figure with a single axes. - useblit : bool - If True, use blitting to speed up animation. Only available on some - backends. If None, set to True when using Agg backend, otherwise False. Attributes ---------- @@ -164,10 +163,8 @@ class PlotPlugin(Plugin): Window containing image used in measurement. image : array Image used in measurement/manipulation. - overlay : array - Image used in measurement/manipulation. """ - def __init__(self, image_viewer, useblit=None, **kwargs): + def __init__(self, image_viewer, **kwargs): Plugin.__init__(self, image_viewer, **kwargs) # Add plot for displaying intensity profile. self.add_plot() @@ -188,3 +185,37 @@ class PlotPlugin(Plugin): self.fig.patch.set_facecolor(bgcolor) self.ax = self.canvas.ax self.layout.addWidget(self.canvas, self.row, 0) + + +class OverlayPlugin(Plugin): + """Plugin for ImageViewer that displays an overlay on top of main image. + + Attributes + ---------- + overlay : array + Overlay displayed on top of image. This overlay defaults to a color map + with alpha values varying linearly from 0 to 1. + """ + + def __init__(self, image_viewer, **kwargs): + Plugin.__init__(self, image_viewer, **kwargs) + self.overlay_cmap = clear_red + self._overlay_plot = None + self._overlay = None + + @property + def overlay(self): + return self._overlay + + @overlay.setter + def overlay(self, image): + self._overlay = image + ax = self.image_viewer.ax + if image is None: + ax.images.remove(self._overlay_plot) + self._overlay_plot = None + elif self._overlay_plot is None: + self._overlay_plot = ax.imshow(image, cmap=self.overlay_cmap) + else: + self._overlay_plot.set_array(image) + self.image_viewer.redraw() diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index cf3b5253..feeaee0d 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,8 +1,8 @@ -from .base import Plugin +from .base import OverlayPlugin from skimage.filter import canny -class CannyPlugin(Plugin): +class CannyPlugin(OverlayPlugin): name = 'Canny Filter' @@ -22,8 +22,8 @@ class CannyPlugin(Plugin): def callback(self, *args, **kwargs): image = canny(*args, **kwargs) - self.image_viewer.overlay = image + self.overlay = image def closeEvent(self, event): - self.image_viewer.overlay = None + self.overlay = None super(CannyPlugin, self).closeEvent(event) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 1f77915d..b6c1db69 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -3,7 +3,7 @@ import sys from PyQt4 import QtGui, QtCore from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg -from skimage.viewer.utils import figimage, clear_red +from skimage.viewer.utils import figimage qApp = None @@ -46,9 +46,6 @@ class ImageViewer(QtGui.QMainWindow): Image being viewed. Setting this value will update the displayed frame. original_image : array Plugins typically operate on (but don't change) the original image. - overlay : array - Overlay displayed on top of image. This overlay defaults to a color map - with alpha values varying linearly from 0 to 1. plugins : list List of attached plugins. """ @@ -61,8 +58,6 @@ class ImageViewer(QtGui.QMainWindow): #TODO: Add ImageViewer to skimage.io window manager - self.overlay_cmap = clear_red - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.setWindowTitle("Image Viewer") @@ -80,8 +75,6 @@ class ImageViewer(QtGui.QMainWindow): self.ax.autoscale(enable=False) self._image_plot = self.ax.images[0] - self._overlay_plot = None - self._overlay = None self.original_image = image self.image = image @@ -127,22 +120,6 @@ class ImageViewer(QtGui.QMainWindow): self._image_plot.set_array(image) self.redraw() - @property - def overlay(self): - return self._overlay - - @overlay.setter - def overlay(self, image): - self._overlay = image - if image is None: - self.ax.images.remove(self._overlay_plot) - self._overlay_plot = None - elif self._overlay_plot is None: - self._overlay_plot = self.ax.imshow(image, cmap=self.overlay_cmap) - else: - self._overlay_plot.set_array(image) - self.canvas.draw_idle() - def connect_event(self, event, callback): """Connect callback function to matplotlib event and return id.""" cid = self.canvas.mpl_connect(event, callback) From 51711213f733a1adc73bbb29bb6ce3916aff8a22 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 20:45:54 -0500 Subject: [PATCH 085/648] Move OverlayPlugin and PlotPlugin to their own modules. --- skimage/viewer/plugins/base.py | 96 ------------------------- skimage/viewer/plugins/canny.py | 2 +- skimage/viewer/plugins/lineprofile.py | 2 +- skimage/viewer/plugins/overlayplugin.py | 37 ++++++++++ skimage/viewer/plugins/plotplugin.py | 63 ++++++++++++++++ 5 files changed, 102 insertions(+), 98 deletions(-) create mode 100644 skimage/viewer/plugins/overlayplugin.py create mode 100644 skimage/viewer/plugins/plotplugin.py diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index eb138295..07b74f22 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,32 +1,8 @@ from PyQt4 import QtGui -import numpy as np import matplotlib as mpl -import matplotlib.pyplot as plt -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg from skimage.io._plugins.q_color_mixer import IntelligentSlider -from skimage.viewer.utils import clear_red - - -class PlotCanvas(FigureCanvasQTAgg): - """Canvas for displaying images. - - This canvas derives from Matplotlib, and has attributes `fig` and `ax`, - which point to Matplotlib figure and axes. - """ - def __init__(self, parent, height, width, **kwargs): - print height, width - self.fig, self.ax = plt.subplots(figsize=(height, width), **kwargs) - - FigureCanvasQTAgg.__init__(self, self.fig) - FigureCanvasQTAgg.setSizePolicy(self, - QtGui.QSizePolicy.Expanding, - QtGui.QSizePolicy.Expanding) - FigureCanvasQTAgg.updateGeometry(self) - # Note: `setParent` must be called after `FigureCanvasQTAgg.__init__`. - self.setParent(parent) - class Plugin(QtGui.QDialog): """Base class for widgets that interact with the axes. @@ -147,75 +123,3 @@ class Plugin(QtGui.QDialog): """Disconnect artists that are connected to the *image plot*.""" for a in self.artists: self.image_viewer.remove_artist(a) - - -class PlotPlugin(Plugin): - """Plugin for ImageViewer that contains a plot Canvas. - - Parameters - ---------- - image_viewer : ImageViewer instance. - Window containing image used in measurement/manipulation. - - Attributes - ---------- - image_viewer : ImageViewer - Window containing image used in measurement. - image : array - Image used in measurement/manipulation. - """ - def __init__(self, image_viewer, **kwargs): - Plugin.__init__(self, image_viewer, **kwargs) - # Add plot for displaying intensity profile. - self.add_plot() - - def redraw(self): - """Redraw plot.""" - self.canvas.draw_idle() - - def add_plot(self, height=4, width=4): - self.canvas = PlotCanvas(self, height, width) - self.fig = self.canvas.fig - #TODO: Converted color is slightly different than Qt background. - qpalette = QtGui.QPalette() - qcolor = qpalette.color(QtGui.QPalette.Window) - bgcolor = qcolor.toRgb().value() - if np.isscalar(bgcolor): - bgcolor = str(bgcolor / 255.) - self.fig.patch.set_facecolor(bgcolor) - self.ax = self.canvas.ax - self.layout.addWidget(self.canvas, self.row, 0) - - -class OverlayPlugin(Plugin): - """Plugin for ImageViewer that displays an overlay on top of main image. - - Attributes - ---------- - overlay : array - Overlay displayed on top of image. This overlay defaults to a color map - with alpha values varying linearly from 0 to 1. - """ - - def __init__(self, image_viewer, **kwargs): - Plugin.__init__(self, image_viewer, **kwargs) - self.overlay_cmap = clear_red - self._overlay_plot = None - self._overlay = None - - @property - def overlay(self): - return self._overlay - - @overlay.setter - def overlay(self, image): - self._overlay = image - ax = self.image_viewer.ax - if image is None: - ax.images.remove(self._overlay_plot) - self._overlay_plot = None - elif self._overlay_plot is None: - self._overlay_plot = ax.imshow(image, cmap=self.overlay_cmap) - else: - self._overlay_plot.set_array(image) - self.image_viewer.redraw() diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index feeaee0d..25500e01 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,5 +1,5 @@ -from .base import OverlayPlugin from skimage.filter import canny +from .overlayplugin import OverlayPlugin class CannyPlugin(OverlayPlugin): diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 485839d0..32ea967d 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -2,7 +2,7 @@ import numpy as np import scipy.ndimage as ndi from skimage.util.dtype import dtype_range -from .base import PlotPlugin +from .plotplugin import PlotPlugin __all__ = ['LineProfile'] diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py new file mode 100644 index 00000000..a2b12630 --- /dev/null +++ b/skimage/viewer/plugins/overlayplugin.py @@ -0,0 +1,37 @@ +from ..utils import clear_red + +from .base import Plugin + + +class OverlayPlugin(Plugin): + """Plugin for ImageViewer that displays an overlay on top of main image. + + Attributes + ---------- + overlay : array + Overlay displayed on top of image. This overlay defaults to a color map + with alpha values varying linearly from 0 to 1. + """ + + def __init__(self, image_viewer, **kwargs): + Plugin.__init__(self, image_viewer, **kwargs) + self.overlay_cmap = clear_red + self._overlay_plot = None + self._overlay = None + + @property + def overlay(self): + return self._overlay + + @overlay.setter + def overlay(self, image): + self._overlay = image + ax = self.image_viewer.ax + if image is None: + ax.images.remove(self._overlay_plot) + self._overlay_plot = None + elif self._overlay_plot is None: + self._overlay_plot = ax.imshow(image, cmap=self.overlay_cmap) + else: + self._overlay_plot.set_array(image) + self.image_viewer.redraw() diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py new file mode 100644 index 00000000..38762dc4 --- /dev/null +++ b/skimage/viewer/plugins/plotplugin.py @@ -0,0 +1,63 @@ +import numpy as np +from PyQt4 import QtGui + +import matplotlib.pyplot as plt +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg + +from .base import Plugin + + +class PlotCanvas(FigureCanvasQTAgg): + """Canvas for displaying images. + + This canvas derives from Matplotlib, and has attributes `fig` and `ax`, + which point to Matplotlib figure and axes. + """ + def __init__(self, parent, height, width, **kwargs): + self.fig, self.ax = plt.subplots(figsize=(height, width), **kwargs) + + FigureCanvasQTAgg.__init__(self, self.fig) + FigureCanvasQTAgg.setSizePolicy(self, + QtGui.QSizePolicy.Expanding, + QtGui.QSizePolicy.Expanding) + FigureCanvasQTAgg.updateGeometry(self) + # Note: `setParent` must be called after `FigureCanvasQTAgg.__init__`. + self.setParent(parent) + + +class PlotPlugin(Plugin): + """Plugin for ImageViewer that contains a plot Canvas. + + Parameters + ---------- + image_viewer : ImageViewer instance. + Window containing image used in measurement/manipulation. + + Attributes + ---------- + image_viewer : ImageViewer + Window containing image used in measurement. + image : array + Image used in measurement/manipulation. + """ + def __init__(self, image_viewer, **kwargs): + Plugin.__init__(self, image_viewer, **kwargs) + # Add plot for displaying intensity profile. + self.add_plot() + + def redraw(self): + """Redraw plot.""" + self.canvas.draw_idle() + + def add_plot(self, height=4, width=4): + self.canvas = PlotCanvas(self, height, width) + self.fig = self.canvas.fig + #TODO: Converted color is slightly different than Qt background. + qpalette = QtGui.QPalette() + qcolor = qpalette.color(QtGui.QPalette.Window) + bgcolor = qcolor.toRgb().value() + if np.isscalar(bgcolor): + bgcolor = str(bgcolor / 255.) + self.fig.patch.set_facecolor(bgcolor) + self.ax = self.canvas.ax + self.layout.addWidget(self.canvas, self.row, 0) From 4d747720de210525aa8e3d7c36da38e3a71f3067 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 22:03:46 -0500 Subject: [PATCH 086/648] Change ImageViewer to automatically call plugins. --- skimage/viewer/viewers/core.py | 2 ++ viewer_examples/plugins/canny.py | 3 +-- viewer_examples/plugins/lineprofile.py | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index b6c1db69..6ef83d27 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -104,6 +104,8 @@ class ImageViewer(QtGui.QMainWindow): self.close() def show(self): + for p in self.plugins: + p.show() super(ImageViewer, self).show() sys.exit(qApp.exec_()) diff --git a/viewer_examples/plugins/canny.py b/viewer_examples/plugins/canny.py index d5e858ec..e84a8270 100644 --- a/viewer_examples/plugins/canny.py +++ b/viewer_examples/plugins/canny.py @@ -5,6 +5,5 @@ from skimage.viewer.plugins.canny import CannyPlugin image = data.camera() viewer = ImageViewer(image) -p = CannyPlugin(viewer) -p.show() +CannyPlugin(viewer) viewer.show() diff --git a/viewer_examples/plugins/lineprofile.py b/viewer_examples/plugins/lineprofile.py index cecb2539..310c23e4 100644 --- a/viewer_examples/plugins/lineprofile.py +++ b/viewer_examples/plugins/lineprofile.py @@ -5,6 +5,5 @@ from skimage.viewer.plugins.lineprofile import LineProfile image = data.camera() viewer = ImageViewer(image) -p = LineProfile(viewer) -p.show() +LineProfile(viewer) viewer.show() From 31c2810dee399c1f9c4dcbb1dd5ac75129e59e6a Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 23:27:05 -0400 Subject: [PATCH 087/648] ENH: Align image and plugin windows --- skimage/viewer/viewers/core.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 6ef83d27..f4d3b587 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -103,7 +103,19 @@ class ImageViewer(QtGui.QMainWindow): def closeEvent(self, ce): self.close() + def auto_layout(self): + """Move viewer to top-left and align plugin on right edge of viewer.""" + size = self.geometry() + self.move(0, 0) + w = size.width() + y = 0 + #TODO: Layout isn't correct for multiple plots (overlaps). + for p in self.plugins: + p.move(w, y) + y += p.geometry().height() + def show(self): + self.auto_layout() for p in self.plugins: p.show() super(ImageViewer, self).show() From bc6c81606ffebd82ecd6f2b2688fd9b24e895883 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 01:25:58 -0400 Subject: [PATCH 088/648] Fix `add_argument`. `arguments` is a list, but I was treating it like a dict. --- skimage/viewer/plugins/base.py | 2 +- skimage/viewer/plugins/canny.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 07b74f22..e4bcd17b 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -80,7 +80,7 @@ class Plugin(QtGui.QDialog): def add_argument(self, name, low, high, callback, **kwargs): name, slider = self.add_slider(name, low, high, callback, **kwargs) - self.arguments[name] = slider + self.arguments.append(slider) def add_keyword_argument(self, name, low, high, callback, **kwargs): name, slider = self.add_slider(name, low, high, callback, **kwargs) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 25500e01..376638e5 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,4 +1,5 @@ from skimage.filter import canny + from .overlayplugin import OverlayPlugin From 887a9119b294bf5ba702603dc15014ea21fe3435 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 02:02:29 -0400 Subject: [PATCH 089/648] ENH: Simplify creation of Slider widget. --- skimage/viewer/plugins/base.py | 23 +++++++++++++++-------- skimage/viewer/plugins/canny.py | 9 +++------ skimage/viewer/widgets/__init__.py | 1 + skimage/viewer/widgets/core.py | 21 +++++++++++++++++++++ 4 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 skimage/viewer/widgets/__init__.py create mode 100644 skimage/viewer/widgets/core.py diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index e4bcd17b..1005384a 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,7 +1,7 @@ from PyQt4 import QtGui import matplotlib as mpl -from skimage.io._plugins.q_color_mixer import IntelligentSlider +from ..widgets import Slider class Plugin(QtGui.QDialog): @@ -78,17 +78,17 @@ class Plugin(QtGui.QDialog): else: return param - def add_argument(self, name, low, high, callback, **kwargs): - name, slider = self.add_slider(name, low, high, callback, **kwargs) + def add_argument(self, name, low, high, **kwargs): + name, slider = self.add_slider(name, low, high, **kwargs) self.arguments.append(slider) - def add_keyword_argument(self, name, low, high, callback, **kwargs): - name, slider = self.add_slider(name, low, high, callback, **kwargs) + def add_keyword_argument(self, name, low, high, **kwargs): + name, slider = self.add_slider(name, low, high, **kwargs) self.keyword_arguments[name] = slider - def add_slider(self, name, low, high, callback, **kwargs): - slider = IntelligentSlider(name, low, high, callback, - orientation='horizontal', **kwargs) + def add_slider(self, name, low, high, **kwargs): + slider = Slider(name, low, high, **kwargs) + slider.callback = self.caller self.layout.addWidget(slider, self.row, 0) self.row += 1 return name.replace(' ', '_'), slider @@ -110,6 +110,13 @@ class Plugin(QtGui.QDialog): This should be used in lieu of `figure.canvas.mpl_connect` since this function stores call back ids for later clean up. + + Parameters + ---------- + event : str + Matplotlib event. + callback : function + Callback function with a matplotlib Event object as its argument. """ cid = self.image_viewer.connect_event(event, callback) self.cids.append(cid) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 376638e5..c93d5b0d 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -12,12 +12,9 @@ class CannyPlugin(OverlayPlugin): width = kwargs.get('width', 400) super(CannyPlugin, self).__init__(image_viewer, width=width, height=height) - self.add_keyword_argument('sigma', 0.005, 0, self.caller, - update_on='release') - self.add_keyword_argument('low_threshold', 0.255, 0, self.caller, - update_on='release') - self.add_keyword_argument('high_threshold', 0.255, 0, self.caller, - update_on='release') + self.add_keyword_argument('sigma', 0, 5, update_on='release') + self.add_keyword_argument('low_threshold', 0, 255, update_on='release') + self.add_keyword_argument('high_threshold', 0, 255, update_on='release') # Call callback so that image is updated to slider values. self.caller() diff --git a/skimage/viewer/widgets/__init__.py b/skimage/viewer/widgets/__init__.py new file mode 100644 index 00000000..5af24064 --- /dev/null +++ b/skimage/viewer/widgets/__init__.py @@ -0,0 +1 @@ +from core import * diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py new file mode 100644 index 00000000..e68bda7f --- /dev/null +++ b/skimage/viewer/widgets/core.py @@ -0,0 +1,21 @@ +from skimage.io._plugins.q_color_mixer import IntelligentSlider + +class Slider(IntelligentSlider): + """Slider widget. + + Parameters + ---------- + name : str + Name of slider parameter. If this parameter is passed as a keyword + argument, it must match the name of that keyword argument. In addition, + this name is displayed as the name of the slider. + low, high : float + Range of slider values. + ptype : {'arg' | 'kwarg' | ...} + Parameter + """ + def __init__(self, name, low, high, ptype='kwarg', callback=None, **kwargs): + self.ptype = ptype + kwargs.setdefault('orientation', 'horizontal') + scale = (high - low) / 1000.0 + super(Slider, self).__init__(name, scale, low, callback, **kwargs) From 9b4c6222b52c5aa2c5e37356d6652c00c05efe56 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 02:08:16 -0400 Subject: [PATCH 090/648] ENH: Rename callback functions for clarity. --- skimage/viewer/plugins/base.py | 12 ++++++------ skimage/viewer/plugins/canny.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 1005384a..14fda815 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -30,7 +30,7 @@ class Plugin(QtGui.QDialog): name = 'Plugin' draws_on_image = False - def __init__(self, image_viewer, callback=None, height=100, width=400, + def __init__(self, image_viewer, image_filter=None, height=100, width=400, useblit=None): self.image_viewer = image_viewer QtGui.QDialog.__init__(self, image_viewer) @@ -40,8 +40,8 @@ class Plugin(QtGui.QDialog): self.layout = QtGui.QGridLayout(self) self.resize(width, height) self.row = 0 - if callback is not None: - self.callback = callback + if image_filter is not None: + self.image_filter = image_filter self.arguments = [image_viewer.original_image] self.keyword_arguments= {} @@ -66,11 +66,11 @@ class Plugin(QtGui.QDialog): bbox = self.image_viewer.ax.bbox self.img_background = self.image_viewer.canvas.copy_from_bbox(bbox) - def caller(self, *args): + def filter_image(self, *args): arguments = [self._get_value(a) for a in self.arguments] kwargs = dict([(name, self._get_value(a)) for name, a in self.keyword_arguments.iteritems()]) - self.callback(*arguments, **kwargs) + self.image_filter(*arguments, **kwargs) def _get_value(self, param): if hasattr(param, 'val'): @@ -88,7 +88,7 @@ class Plugin(QtGui.QDialog): def add_slider(self, name, low, high, **kwargs): slider = Slider(name, low, high, **kwargs) - slider.callback = self.caller + slider.callback = self.filter_image self.layout.addWidget(slider, self.row, 0) self.row += 1 return name.replace(' ', '_'), slider diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index c93d5b0d..08099795 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -15,10 +15,10 @@ class CannyPlugin(OverlayPlugin): self.add_keyword_argument('sigma', 0, 5, update_on='release') self.add_keyword_argument('low_threshold', 0, 255, update_on='release') self.add_keyword_argument('high_threshold', 0, 255, update_on='release') - # Call callback so that image is updated to slider values. - self.caller() + # Update image overlay to default slider values. + self.filter_image() - def callback(self, *args, **kwargs): + def image_filter(self, *args, **kwargs): image = canny(*args, **kwargs) self.overlay = image From 385382f64a0bf49f0583cfa1babf77132c5c06dd Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 02:11:56 -0400 Subject: [PATCH 091/648] ENH: Simplify widget addition. --- skimage/viewer/plugins/base.py | 17 +++++++---------- skimage/viewer/plugins/canny.py | 6 +++--- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 14fda815..22ec3533 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -78,17 +78,14 @@ class Plugin(QtGui.QDialog): else: return param - def add_argument(self, name, low, high, **kwargs): - name, slider = self.add_slider(name, low, high, **kwargs) - self.arguments.append(slider) - - def add_keyword_argument(self, name, low, high, **kwargs): - name, slider = self.add_slider(name, low, high, **kwargs) - self.keyword_arguments[name] = slider - - def add_slider(self, name, low, high, **kwargs): + def add_widget(self, name, low, high, **kwargs): slider = Slider(name, low, high, **kwargs) - slider.callback = self.filter_image + if slider.ptype == 'kwarg': + self.keyword_arguments[name] = slider + slider.callback = self.filter_image + elif slider.ptype == 'arg': + self.keyword_arguments[name] = slider + self.arguments.append(slider) self.layout.addWidget(slider, self.row, 0) self.row += 1 return name.replace(' ', '_'), slider diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 08099795..278a7931 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -12,9 +12,9 @@ class CannyPlugin(OverlayPlugin): width = kwargs.get('width', 400) super(CannyPlugin, self).__init__(image_viewer, width=width, height=height) - self.add_keyword_argument('sigma', 0, 5, update_on='release') - self.add_keyword_argument('low_threshold', 0, 255, update_on='release') - self.add_keyword_argument('high_threshold', 0, 255, update_on='release') + self.add_widget('sigma', 0, 5, update_on='release') + self.add_widget('low_threshold', 0, 255, update_on='release') + self.add_widget('high_threshold', 0, 255, update_on='release') # Update image overlay to default slider values. self.filter_image() From 3271e210beddd1fd1fd953af1111810a589f9479 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 02:15:16 -0400 Subject: [PATCH 092/648] ENH: Move closeEvent definition to base class. --- skimage/viewer/plugins/canny.py | 4 ---- skimage/viewer/plugins/overlayplugin.py | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 278a7931..020157a4 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -21,7 +21,3 @@ class CannyPlugin(OverlayPlugin): def image_filter(self, *args, **kwargs): image = canny(*args, **kwargs) self.overlay = image - - def closeEvent(self, event): - self.overlay = None - super(CannyPlugin, self).closeEvent(event) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index a2b12630..b55d6587 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -35,3 +35,7 @@ class OverlayPlugin(Plugin): else: self._overlay_plot.set_array(image) self.image_viewer.redraw() + + def closeEvent(self, event): + self.overlay = None + super(OverlayPlugin, self).closeEvent(event) From f971c25a0b8a1e0a29276c6e899c6a5f9f00b44c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 22 Jul 2012 08:21:31 +0200 Subject: [PATCH 093/648] remove some blank lines --- skimage/transform/_geometric.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 0af7ffb1..fe829109 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -52,7 +52,6 @@ class GeometricTransform(object): """ raise NotImplementedError() - def inverse(self, coords): """Apply inverse transformation. @@ -69,7 +68,6 @@ class GeometricTransform(object): """ raise NotImplementedError() - def __add__(self, other): """Combine this transformation with another. @@ -131,7 +129,6 @@ class ProjectiveTransform(GeometricTransform): def inverse(self, coords): return self._apply_mat(coords, self._inv_matrix) - def estimate(self, src, dst): """Set the transformation matrix with the explicit transformation parameters. @@ -185,7 +182,6 @@ class ProjectiveTransform(GeometricTransform): "types.") - class AffineTransform(ProjectiveTransform): """2D affine transformation of the form:: @@ -560,4 +556,3 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # The spline filters sometimes return results outside [0, 1], # so clip to ensure valid data return np.clip(mapped.squeeze(), 0, 1) - From 06449581bd4f1a7adbd8897ed2a244c660204980 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 02:26:03 -0400 Subject: [PATCH 094/648] ENH: Generalize add_widget function. --- skimage/viewer/plugins/base.py | 19 +++++++++---------- skimage/viewer/plugins/canny.py | 7 ++++--- skimage/viewer/widgets/core.py | 5 +++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 22ec3533..43ad4c0a 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -78,17 +78,16 @@ class Plugin(QtGui.QDialog): else: return param - def add_widget(self, name, low, high, **kwargs): - slider = Slider(name, low, high, **kwargs) - if slider.ptype == 'kwarg': - self.keyword_arguments[name] = slider - slider.callback = self.filter_image - elif slider.ptype == 'arg': - self.keyword_arguments[name] = slider - self.arguments.append(slider) - self.layout.addWidget(slider, self.row, 0) + def add_widget(self, widget): + if widget.ptype == 'kwarg': + name = widget.name.replace(' ', '_') + self.keyword_arguments[name] = widget + widget.callback = self.filter_image + elif widget.ptype == 'arg': + self.arguments.append(widget) + widget.callback = self.filter_image + self.layout.addWidget(widget, self.row, 0) self.row += 1 - return name.replace(' ', '_'), slider def closeEvent(self, event): """Disconnect all artists and events from ImageViewer. diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 020157a4..175b749d 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,6 +1,7 @@ from skimage.filter import canny from .overlayplugin import OverlayPlugin +from ..widgets import Slider class CannyPlugin(OverlayPlugin): @@ -12,9 +13,9 @@ class CannyPlugin(OverlayPlugin): width = kwargs.get('width', 400) super(CannyPlugin, self).__init__(image_viewer, width=width, height=height) - self.add_widget('sigma', 0, 5, update_on='release') - self.add_widget('low_threshold', 0, 255, update_on='release') - self.add_widget('high_threshold', 0, 255, update_on='release') + self.add_widget(Slider('sigma', 0, 5, update_on='release')) + self.add_widget(Slider('low threshold', 0, 255, update_on='release')) + self.add_widget(Slider('high threshold', 0, 255, update_on='release')) # Update image overlay to default slider values. self.filter_image() diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index e68bda7f..9184ccf6 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -7,8 +7,9 @@ class Slider(IntelligentSlider): ---------- name : str Name of slider parameter. If this parameter is passed as a keyword - argument, it must match the name of that keyword argument. In addition, - this name is displayed as the name of the slider. + argument, it must match the name of that keyword argument (spaces are + replaced with underscores). In addition, this name is displayed as the + name of the slider. low, high : float Range of slider values. ptype : {'arg' | 'kwarg' | ...} From 0f5d6153d24ca7cbe5dda1a1bac991cf862a566a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 22 Jul 2012 08:58:37 +0200 Subject: [PATCH 095/648] fix bug in estimation of similarity transformation --- skimage/transform/_geometric.py | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index fe829109..70584664 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -271,6 +271,40 @@ class SimilarityTransform(AffineTransform): shear=0, translation=translation) + def estimate(self, src, dst): + """Set the transformation matrix with the explicit transformation + parameters. + + Parameters + ---------- + src : Nx2 array + source coordinates + dst : Nx2 array + destination coordinates + + """ + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] + rows = src.shape[0] + + #: params: a0, a1, b0, b1 + A = np.zeros((rows * 2, 4)) + A[:rows, 0] = xs + A[:rows, 2] = - ys + A[:rows, 1] = 1 + A[rows:, 2] = xs + A[rows:, 0] = ys + A[rows:, 3] = 1 + + b = np.hstack([xd, yd]) + + a0, a1, b0, b1 = np.linalg.lstsq(A, b)[0] + self._matrix = np.array([[a0, -b0, a1], + [b0, a0, b1], + [ 0, 0, 1]]) + class PolynomialTransform(GeometricTransform): """2D transformation of the form:: From 87c57770ba51f9727f455ebe16fe0b2753182f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 22 Jul 2012 16:24:58 +0200 Subject: [PATCH 096/648] reimplement implicit parameter functionality of transformations --- skimage/transform/_geometric.py | 142 +++++++++++++++------- skimage/transform/tests/test_geometric.py | 39 +++++- 2 files changed, 134 insertions(+), 47 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 70584664..a47b51e4 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -30,11 +30,6 @@ def _stackcopy(a, b): class GeometricTransform(object): """Perform geometric transformations on a set of coordinates. - Parameters - ---------- - matrix : 3x3 array, optional - Homogeneous transformation matrix. - """ def __call__(self, coords): """Apply forward transformation. @@ -99,6 +94,11 @@ class ProjectiveTransform(GeometricTransform): [0 1 20] [0 0 1 ]]. + Parameters + ---------- + matrix : 3x3 array, optional + Homogeneous transformation matrix. + """ _coefs = range(8) @@ -201,22 +201,30 @@ class AffineTransform(ProjectiveTransform): Parameters ---------- - scale : (sx, sy), floats - Scale factors. - rotation : float - Rotation angle in radians, counter-clockwise direction. - shear : float - Shear angle in radians, counter-clockwise direction. - translation : (tx, ty), floats - Translation in x and y. + matrix : 3x3 array, optional + Homogeneous transformation matrix. """ _coefs = range(6) - def __init__(self, scale=None, rotation=None, shear=None, translation=None): - ProjectiveTransform.__init__(self) + def compose_implicit(self, scale=None, rotation=None, shear=None, + translation=None): + """Set the transformation matrix with the implicit transformation + parameters. + Parameters + ---------- + scale : (sx, sy) as array, list or tuple + scale factors + rotation : float + rotation angle in counter-clockwise direction + shear : float + shear angle in counter-clockwise direction + translation : (tx, ty) as array, list or tuple + translation parameters + + """ if scale is None: scale = (1, 1) if rotation is None: @@ -226,18 +234,35 @@ class AffineTransform(ProjectiveTransform): if translation is None: translation = (0, 0) - a = rotation sx, sy = scale - tx, ty = translation - self._matrix = np.array([ - [sx * math.cos(a), - sy * math.sin(a + shear), tx], - [sx * math.sin(a), sy * math.cos(a + shear), ty], - [0, 0, 1] + [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 + + @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) + return sx, sy + + @property + def rotation(self): + return math.atan2(self._matrix[1, 0], self._matrix[0, 0]) + + @property + def shear(self): + beta = math.atan2(- self._matrix[0, 1], self._matrix[1, 1]) + return beta - self.rotation + + @property + def translation(self): + return self._matrix[0:2, 2] -class SimilarityTransform(AffineTransform): +class SimilarityTransform(ProjectiveTransform): """2D similarity transformation of the form:: X = a0*x + b0*y + a1 = @@ -254,23 +279,11 @@ class SimilarityTransform(AffineTransform): Parameters ---------- - scale : float, optional - Scale / zoom factor. - rotation : float, optional - Rotation angle, counter-clockwise, in radians. - translation : (tx, ty) of float - x, y translation parameters + matrix : 3x3 array, optional + Homogeneous transformation matrix. """ - def __init__(self, scale=None, rotation=None, translation=None): - if scale is not None: - scale = (scale, scale) - AffineTransform.__init__(self, scale=scale, - rotation=rotation, - shear=0, - translation=translation) - def estimate(self, src, dst): """Set the transformation matrix with the explicit transformation parameters. @@ -305,6 +318,52 @@ class SimilarityTransform(AffineTransform): [b0, a0, b1], [ 0, 0, 1]]) + def compose_implicit(self, scale=None, rotation=None, translation=None): + """Set the transformation matrix with the implicit transformation + parameters. + + Parameters + ---------- + scale : float, optional + scale factor + rotation : float, optional + rotation angle in counter-clockwise direction + translation : (tx, ty) as array, list or tuple, optional + x, y translation parameters + + """ + if scale is None: + scale = (1, 1) + if rotation is None: + rotation = 0 + if translation is None: + translation = (0, 0) + + self._matrix = np.array([ + [math.cos(rotation), - math.sin(rotation), 0], + [math.sin(rotation), math.cos(rotation), 0], + [ 0, 0, 1] + ]) + self._matrix *= scale + self._matrix[0:2, 2] = translation + + @property + def scale(self): + if math.cos(self.rotation) == 0: + # sin(self.rotation) == 1 + scale = self._matrix[0, 1] + else: + scale = self._matrix[0, 0] / math.cos(self.rotation) + return scale + + @property + def rotation(self): + return math.atan2(self._matrix[1, 0], self._matrix[1, 1]) + + @property + def translation(self): + return self._matrix[0:2, 2] + class PolynomialTransform(GeometricTransform): """2D transformation of the form:: @@ -449,7 +508,7 @@ def estimate_transform(ttype, src, dst, **kwargs): >>> tform = tf.estimate_transform('similarity', src, dst) - >>> tform.inverse(tform.forward(src)) # == src + >>> tform.inverse(tform(src)) # == src >>> # warp image using the estimated transformation >>> from skimage import data @@ -458,15 +517,12 @@ def estimate_transform(ttype, src, dst, **kwargs): >>> warp(image, inverse_map=tform.inverse) >>> # create transformation with explicit parameters - >>> scale = 1.1 - >>> rotation = 1 - >>> translation = (10, 20) - >>> - >>> tform2 = tf.SimilarityTransform(scale, rotation, translation) + >>> tform2 = tf.SimilarityTransform() + >>> tform2.compose_implicit(scale=1.1, rotation=1, translation=(10, 20)) >>> # unite transformations, applied in order from left to right >>> tform3 = tform + tform2 - >>> tform3.forward(src) # == tform2.forward(tform.forward(src)) + >>> tform3(src) # == tform2(tform(src)) """ ttype = ttype.lower() diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index 4430673c..f99bab7a 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import assert_array_almost_equal +from numpy.testing import assert_equal, assert_array_almost_equal from skimage.transform._geometric import _stackcopy from skimage.transform import (estimate_transform, SimilarityTransform, @@ -43,12 +43,26 @@ def test_similarity_estimation(): tform = estimate_transform('similarity', SRC[:2, :], DST[:2, :]) assert_array_almost_equal(tform(SRC[:2, :]), DST[:2, :]) assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) + assert_equal(tform._matrix[0, 0], tform._matrix[1, 1]) + assert_equal(tform._matrix[0, 1], - tform._matrix[1, 0]) #: over-determined tform = estimate_transform('similarity', SRC, DST) assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) + assert_equal(tform._matrix[0, 0], tform._matrix[1, 1]) + assert_equal(tform._matrix[0, 1], - tform._matrix[1, 0]) +def test_similarity_implicit(): + tform = SimilarityTransform() + scale = 0.1 + rotation = 1 + translation = (1, 1) + tform.compose_implicit(scale, rotation, 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 @@ -61,6 +75,19 @@ def test_affine_estimation(): assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) +def test_affine_implicit(): + tform = AffineTransform() + scale = (0.1, 0.13) + rotation = 1 + shear = 0.1 + translation = (1, 1) + tform.compose_implicit(scale, rotation, shear, translation) + assert_array_almost_equal(tform.scale, scale) + assert_array_almost_equal(tform.rotation, rotation) + assert_array_almost_equal(tform.shear, shear) + assert_array_almost_equal(tform.translation, translation) + + def test_projective(): #: exact solution tform = estimate_transform('projective', SRC[:4, :], DST[:4, :]) @@ -77,9 +104,13 @@ def test_polynomial(): def test_union(): - tform1 = SimilarityTransform(1, 0.3) - tform2 = SimilarityTransform(1, 0.6) - tform3 = SimilarityTransform(1, 0.9) + tform1 = SimilarityTransform() + tform1.compose_implicit(scale=0.1, rotation=0.3) + tform2 = SimilarityTransform() + tform2.compose_implicit(scale=0.1, rotation=0.9) + tform3 = SimilarityTransform() + tform3.compose_implicit(scale=0.1**2, rotation=0.3+0.9) + tform = tform1 + tform2 From be6bb0c809da448a846f6092cfc9085c74380f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 22 Jul 2012 16:38:25 +0200 Subject: [PATCH 097/648] combination of two transformations of the same type result in this type again --- skimage/transform/_geometric.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index a47b51e4..087e4ae6 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -176,7 +176,13 @@ class ProjectiveTransform(GeometricTransform): """ if isinstance(other, ProjectiveTransform): - return ProjectiveTransform(np.dot(other._matrix, self._matrix)) + # combination of the same types result in a transformation of this + # type again, otherwise use general projective transformation + if type(self) == type(other): + tform = self.__class__ + else: + tform = ProjectiveTransform + return tform(other._matrix.dot(self._matrix)) else: raise TypeError("Cannot combine transformations of differing " "types.") From 2ae4dd4551801b92b740de19f8f383022a13bf28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 22 Jul 2012 16:41:58 +0200 Subject: [PATCH 098/648] fix scale initialization in implicit composition of similarity --- skimage/transform/_geometric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 087e4ae6..1058315c 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -339,7 +339,7 @@ class SimilarityTransform(ProjectiveTransform): """ if scale is None: - scale = (1, 1) + scale = 1 if rotation is None: rotation = 0 if translation is None: From d9a88c95b5c5f19f374fcac2dd69da3ae8759a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 22 Jul 2012 16:52:54 +0200 Subject: [PATCH 099/648] add doc for and restructure polynomial coefficients --- skimage/transform/_geometric.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 1058315c..c1c39b2b 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -374,24 +374,19 @@ class SimilarityTransform(ProjectiveTransform): class PolynomialTransform(GeometricTransform): """2D transformation of the form:: - X = sum[j=0:n]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) - Y = sum[j=0:n]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) + X = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) + Y = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) - TODO: Describe structure of coefficients. - Shall we store it as a (2, M) ndarray? + Parameters + ---------- + coeffs : 2xN array, optional + Polynomial coefficients where `N * 2 = (order + 1) * (order + 2)`. So, + a_ji is defined in `coeffs[0, :]` and b_ji in `coeffs[1, :]`. """ def __init__(self, coeffs=None): - """Create polynomial transformation. - - Parameters - ---------- - coeffs : array, optional - polynomial coefficients - - """ - self.coeffs = coeffs + self._coeffs = coeffs def estimate(self, src, dst, order): """Set the transformation matrix with the explicit transformation @@ -426,7 +421,7 @@ class PolynomialTransform(GeometricTransform): b = np.hstack([xd, yd]) - self.coeffs = np.linalg.lstsq(A, b)[0] + self._coeffs = np.linalg.lstsq(A, b)[0].reshape((2, u / 2)) def __call__(self, coords): """Apply forward transformation. @@ -444,7 +439,7 @@ class PolynomialTransform(GeometricTransform): """ x = coords[:, 0] y = coords[:, 1] - u = len(self.coeffs.ravel()) + u = len(self._coeffs.ravel()) # number of coefficients -> u = (order + 1) * (order + 2) order = int((- 3 + math.sqrt(9 - 4 * (2 - u))) / 2) dst = np.zeros(coords.shape) @@ -452,8 +447,8 @@ class PolynomialTransform(GeometricTransform): pidx = 0 for j in xrange(order + 1): for i in xrange(j + 1): - dst[:, 0] += self.coeffs[pidx] * x ** (j - i) * y ** i - dst[:, 1] += self.coeffs[pidx + u / 2] * x ** (j - i) * y ** i + dst[:, 0] += self._coeffs[0, pidx] * x ** (j - i) * y ** i + dst[:, 1] += self._coeffs[1, pidx] * x ** (j - i) * y ** i pidx += 1 return dst From 86b428952dbfecb3623fb3daab8889f1340f1ad9 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 13:24:41 -0400 Subject: [PATCH 100/648] ENH: Allow `Plugin.add_widget` to hook into Plugin attributes. The `ptype` parameter of widget can now be set to 'plugin'. When this is the case, the plugin will set a plugin attribute whenever the widget is updated. As an example, this commit adds a ComboBox widget which is hooked into the overlay color of the OverlayPlugin. --- skimage/viewer/plugins/base.py | 7 ++- skimage/viewer/plugins/canny.py | 3 +- skimage/viewer/plugins/overlayplugin.py | 30 ++++++++++-- skimage/viewer/utils/core.py | 7 +-- skimage/viewer/widgets/core.py | 65 ++++++++++++++++++++++++- 5 files changed, 99 insertions(+), 13 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 43ad4c0a..17387b8b 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,7 +1,6 @@ from PyQt4 import QtGui import matplotlib as mpl -from ..widgets import Slider class Plugin(QtGui.QDialog): @@ -43,6 +42,7 @@ class Plugin(QtGui.QDialog): if image_filter is not None: self.image_filter = image_filter + #TODO: Always passing image as first argument may be bad assumption. self.arguments = [image_viewer.original_image] self.keyword_arguments= {} @@ -86,9 +86,14 @@ class Plugin(QtGui.QDialog): elif widget.ptype == 'arg': self.arguments.append(widget) widget.callback = self.filter_image + elif widget.ptype == 'plugin': + widget.callback = self.update_plugin self.layout.addWidget(widget, self.row, 0) self.row += 1 + def update_plugin(self, name, value): + setattr(self, name, value) + def closeEvent(self, event): """Disconnect all artists and events from ImageViewer. diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 175b749d..97465e6a 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,7 +1,7 @@ from skimage.filter import canny from .overlayplugin import OverlayPlugin -from ..widgets import Slider +from ..widgets import Slider, ComboBox class CannyPlugin(OverlayPlugin): @@ -16,6 +16,7 @@ class CannyPlugin(OverlayPlugin): self.add_widget(Slider('sigma', 0, 5, update_on='release')) self.add_widget(Slider('low threshold', 0, 255, update_on='release')) self.add_widget(Slider('high threshold', 0, 255, update_on='release')) + self.add_widget(ComboBox('color', self.color_names, ptype='plugin')) # Update image overlay to default slider values. self.filter_image() diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index b55d6587..e9a6a42a 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -1,6 +1,5 @@ -from ..utils import clear_red - from .base import Plugin +from ..utils import ClearColormap class OverlayPlugin(Plugin): @@ -12,12 +11,19 @@ class OverlayPlugin(Plugin): Overlay displayed on top of image. This overlay defaults to a color map with alpha values varying linearly from 0 to 1. """ + colors = {'red': (1, 0, 0), + 'yellow': (1, 1, 0), + 'green': (0, 1, 0), + 'cyan': (0, 1, 1)} def __init__(self, image_viewer, **kwargs): Plugin.__init__(self, image_viewer, **kwargs) - self.overlay_cmap = clear_red self._overlay_plot = None self._overlay = None + self.cmap = None + self.color_names = self.colors.keys() + #TODO: `color` doesn't update GUI widget when set manually. + self.color = 0 @property def overlay(self): @@ -31,7 +37,7 @@ class OverlayPlugin(Plugin): ax.images.remove(self._overlay_plot) self._overlay_plot = None elif self._overlay_plot is None: - self._overlay_plot = ax.imshow(image, cmap=self.overlay_cmap) + self._overlay_plot = ax.imshow(image, cmap=self.cmap) else: self._overlay_plot.set_array(image) self.image_viewer.redraw() @@ -39,3 +45,19 @@ class OverlayPlugin(Plugin): def closeEvent(self, event): self.overlay = None super(OverlayPlugin, self).closeEvent(event) + + @property + def color(self): + return self._color + + @color.setter + def color(self, index): + # Update colormap whenever color is changed. + name = self.color_names[index] + self._color = name + rgb = self.colors[name] + self.cmap = ClearColormap(rgb) + + if self._overlay_plot is not None: + self._overlay_plot.set_cmap(self.cmap) + self.image_viewer.redraw() diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index f7dc6433..6a29b07c 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -3,7 +3,7 @@ import matplotlib.pyplot as plt from matplotlib.colors import LinearSegmentedColormap -__all__ = ['figimage', 'LinearColormap', 'ClearColormap', 'clear_red'] +__all__ = ['figimage', 'LinearColormap', 'ClearColormap'] def figimage(image, scale=1, dpi=None, **kwargs): @@ -65,13 +65,10 @@ class LinearColormap(LinearSegmentedColormap): class ClearColormap(LinearColormap): """Color map that varies linearly from alpha = 0 to 1 """ - def __init__(self, name, rgb): + def __init__(self, rgb, name='clear_color'): r, g, b = rgb cg_speq = {'blue': [(0.0, b), (1.0, b)], 'green': [(0.0, g), (1.0, g)], 'red': [(0.0, r), (1.0, r)], 'alpha': [(0.0, 0.0), (1.0, 1.0)]} LinearColormap.__init__(self, name, cg_speq) - -clear_red = ClearColormap('clear_red', (0.7, 0, 0)) - diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 9184ccf6..02d29545 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -1,5 +1,21 @@ +""" +Widgets for interacting with ImageViewer. + +These widgets should be added to a Plugin subclass using its `add_widget` +method. The Plugin will delegate action based on the widget's parameter type +specified by its `ptype` attribute, which can be: + + 'arg' : positional argument passed to Plugin's `filter_image` method. + 'kwarg' : keyword argument passed to Plugin's `filter_image` method. + 'plugin' : attribute of Plugin. You'll probably need to make the attribute + a class property that updates the display. + +""" +from PyQt4 import QtGui +from PyQt4 import QtCore from skimage.io._plugins.q_color_mixer import IntelligentSlider + class Slider(IntelligentSlider): """Slider widget. @@ -12,11 +28,56 @@ class Slider(IntelligentSlider): name of the slider. low, high : float Range of slider values. - ptype : {'arg' | 'kwarg' | ...} - Parameter + ptype : {'arg' | 'kwarg' | 'plugin'} + Parameter type. """ def __init__(self, name, low, high, ptype='kwarg', callback=None, **kwargs): self.ptype = ptype kwargs.setdefault('orientation', 'horizontal') scale = (high - low) / 1000.0 super(Slider, self).__init__(name, scale, low, callback, **kwargs) + + +class ComboBox(QtGui.QWidget): + """ComboBox widget for selecting among a list of choices. + + Parameters + ---------- + name : str + Name of slider parameter. If this parameter is passed as a keyword + argument, it must match the name of that keyword argument (spaces are + replaced with underscores). In addition, this name is displayed as the + name of the slider. + items: list + Allowed parameter values. + ptype : {'arg' | 'kwarg' | 'plugin'} + Parameter type. + """ + + def __init__(self, name, items, ptype='kwarg', callback=None): + super(ComboBox, self).__init__() + self.ptype = ptype + self.callback = callback + + self.name = name + self.name_label = QtGui.QLabel() + self.name_label.setText(self.name) + self.name_label.setAlignment(QtCore.Qt.AlignLeft) + + self._combo_box = QtGui.QComboBox() + self._combo_box.addItems(items) + + self.layout = QtGui.QHBoxLayout(self) + self.layout.addWidget(self.name_label) + self.layout.addWidget(self._combo_box, alignment=QtCore.Qt.AlignLeft) + + self._combo_box.currentIndexChanged.connect(self._value_changed) + # self.connect(self._combo_box, + # SIGNAL("currentIndexChanged(int)"), self.updateUi) + + @property + def val(self): + return self._combo_box.value() + + def _value_changed(self, value): + self.callback(self.name, value) From 9d1df0c3ce118b92fcdd6f2190a843adbbb88f54 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 22:11:33 -0400 Subject: [PATCH 101/648] Make alpha value to ClearColormap adjustable. --- skimage/viewer/utils/core.py | 4 ++-- skimage/viewer/viewers/core.py | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 6a29b07c..cdcc10bf 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -65,10 +65,10 @@ class LinearColormap(LinearSegmentedColormap): class ClearColormap(LinearColormap): """Color map that varies linearly from alpha = 0 to 1 """ - def __init__(self, rgb, name='clear_color'): + def __init__(self, rgb, max_alpha=1, name='clear_color'): r, g, b = rgb cg_speq = {'blue': [(0.0, b), (1.0, b)], 'green': [(0.0, g), (1.0, g)], 'red': [(0.0, r), (1.0, r)], - 'alpha': [(0.0, 0.0), (1.0, 1.0)]} + 'alpha': [(0.0, 0.0), (1.0, max_alpha)]} LinearColormap.__init__(self, name, cg_speq) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index f4d3b587..ac004987 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -10,10 +10,7 @@ qApp = None class ImageCanvas(FigureCanvasQTAgg): - """Canvas for displaying images. - - This canvas derives from Matplotlib, so your normal - """ + """Canvas for displaying images.""" def __init__(self, parent, image, **kwargs): self.fig, self.ax = figimage(image, **kwargs) @@ -91,7 +88,6 @@ class ImageViewer(QtGui.QMainWindow): self.layout = QtGui.QVBoxLayout(self.main_widget) self.layout.addWidget(self.canvas) - #TODO: Set status bar to fixed-width font so status doesn't wiggle status_bar = self.statusBar() self.status_message = status_bar.showMessage sb_size = status_bar.sizeHint() @@ -109,7 +105,7 @@ class ImageViewer(QtGui.QMainWindow): self.move(0, 0) w = size.width() y = 0 - #TODO: Layout isn't correct for multiple plots (overlaps). + #TODO: Layout isn't correct for multiple plugins (overlaps). for p in self.plugins: p.move(w, y) y += p.geometry().height() From f47312a3d1f730ca8766dcbe4a2bbefc0b14fcfd Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 23 Jul 2012 00:12:21 -0400 Subject: [PATCH 102/648] API Change: Attach ImageViewer to Plugin after init. Plugin is now added to the viewer using an inplace add on the viewer instead of on initialization of the plugin. This change means that operations requiring the viewer must be delayed until attach operation. --- skimage/viewer/plugins/base.py | 25 +++++++------ skimage/viewer/plugins/canny.py | 13 ++++--- skimage/viewer/plugins/lineprofile.py | 47 +++++++++++++------------ skimage/viewer/plugins/overlayplugin.py | 7 ++-- skimage/viewer/plugins/plotplugin.py | 5 +-- skimage/viewer/viewers/core.py | 4 +++ viewer_examples/plugins/canny.py | 2 +- viewer_examples/plugins/lineprofile.py | 2 +- 8 files changed, 61 insertions(+), 44 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 17387b8b..ba68ecb6 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,4 +1,5 @@ from PyQt4 import QtGui +from PyQt4.QtCore import Qt import matplotlib as mpl @@ -23,17 +24,13 @@ class Plugin(QtGui.QDialog): ---------- image_viewer : ImageViewer Window containing image used in measurement. - image : array - Image used in measurement/manipulation. """ name = 'Plugin' draws_on_image = False - def __init__(self, image_viewer, image_filter=None, height=100, width=400, - useblit=None): - self.image_viewer = image_viewer - QtGui.QDialog.__init__(self, image_viewer) - self.image_viewer.plugins.append(self) + def __init__(self, image_filter=None, height=100, width=400, useblit=None): + QtGui.QDialog.__init__(self) + self.image_viewer = None self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) @@ -42,18 +39,24 @@ class Plugin(QtGui.QDialog): if image_filter is not None: self.image_filter = image_filter - #TODO: Always passing image as first argument may be bad assumption. - self.arguments = [image_viewer.original_image] + self.arguments = [] self.keyword_arguments= {} - self.image = self.image_viewer.image - if useblit is None: useblit = True if mpl.backends.backend.endswith('Agg') else False self.useblit = useblit self.cids = [] self.artists = [] + def attach(self, image_viewer): + self.setParent(image_viewer) + self.setWindowFlags(Qt.Dialog) + + self.image_viewer = image_viewer + self.image_viewer.plugins.append(self) + #TODO: Always passing image as first argument may be bad assumption. + self.arguments.append(self.image_viewer.original_image) + if self.draws_on_image: self.connect_event('draw_event', self.on_draw) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 97465e6a..5ec6e415 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -8,15 +8,18 @@ class CannyPlugin(OverlayPlugin): name = 'Canny Filter' - def __init__(self, image_viewer, *args, **kwargs): - height = kwargs.get('height', 100) - width = kwargs.get('width', 400) - super(CannyPlugin, self).__init__(image_viewer, - width=width, height=height) + def __init__(self, *args, **kwargs): + kwargs.setdefault('height', 100) + kwargs.setdefault('width', 400) + super(CannyPlugin, self).__init__(**kwargs) + self.add_widget(Slider('sigma', 0, 5, update_on='release')) self.add_widget(Slider('low threshold', 0, 255, update_on='release')) self.add_widget(Slider('high threshold', 0, 255, update_on='release')) self.add_widget(ComboBox('color', self.color_names, ptype='plugin')) + + def attach(self, image_viewer): + super(CannyPlugin, self).attach(image_viewer) # Update image overlay to default slider values. self.filter_image() diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 32ea967d..f41407b3 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -33,43 +33,46 @@ class LineProfile(PlotPlugin): name = 'Line Profile' draws_on_image = True - def __init__(self, image_viewer, useblit=None, - linewidth=1, epsilon=5, limits='image'): - super(LineProfile, self).__init__(image_viewer, height=200, width=600, + def __init__(self, useblit=None, linewidth=1, epsilon=5, limits='image'): + super(LineProfile, self).__init__(height=200, width=600, useblit=useblit) - self.linewidth = linewidth self.epsilon = epsilon + self._active_pt = None + self._limit_type = limits + self.line_kwargs = dict(color='y', lw=linewidth, alpha=0.5, marker='s', + markersize=5, solid_capstyle='butt') + print self.help() - if limits == 'image': - self.limits = (np.min(self.image), np.max(self.image)) - elif limits == 'dtype': - self.limits = dtype_range[self.image.dtype.type] - elif limits is None or len(limits) == 2: - self.limits = limits + def attach(self, image_viewer): + super(LineProfile, self).attach(image_viewer) + + image = image_viewer.original_image + + if self._limit_type == 'image': + self.limits = (np.min(image), np.max(image)) + elif self._limit_type == 'dtype': + self.self._limit_type = dtype_range[image.dtype.type] + elif self._limit_type is None or len(self._limit_type) == 2: + self.limits = self._limit_type else: - raise ValueError("Unrecognized `limits`: %s" % limits) + raise ValueError("Unrecognized `limits`: %s" % self._limit_type) - if not limits is None: + if not self._limit_type is None: self.ax.set_ylim(self.limits) - h, w = self.image.shape - + h, w = image.shape self._init_end_pts = np.array([[w/3, h/2], [2*w/3, h/2]]) self.end_pts = self._init_end_pts.copy() x, y = np.transpose(self.end_pts) - self.scan_line = self.image_viewer.ax.plot(x, y, 'y-s', markersize=5, - lw=linewidth, alpha=0.5, - solid_capstyle='butt')[0] + self.scan_line = image_viewer.ax.plot(x, y, **self.line_kwargs)[0] self.artists.append(self.scan_line) - scan_data = profile_line(self.image, self.end_pts) + scan_data = profile_line(image, self.end_pts) self.profile = self.ax.plot(scan_data, 'k-')[0] self._autoscale_view() - self._active_pt = None - self.connect_event('key_press_event', self.on_key_press) self.connect_event('button_press_event', self.on_mouse_press) self.connect_event('button_release_event', self.on_mouse_release) @@ -77,7 +80,6 @@ class LineProfile(PlotPlugin): self.connect_event('scroll_event', self.on_scroll) self.image_viewer.redraw() - print self.help() def help(self): helpstr = ("Line profile tool", @@ -169,7 +171,8 @@ class LineProfile(PlotPlugin): self.scan_line.set_data(np.transpose(self.end_pts)) self.scan_line.set_linewidth(self.linewidth) - scan = profile_line(self.image, self.end_pts, linewidth=self.linewidth) + scan = profile_line(self.image_viewer.original_image, self.end_pts, + linewidth=self.linewidth) self.profile.set_xdata(np.arange(scan.shape[0])) self.profile.set_ydata(scan) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index e9a6a42a..3b051e1a 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -16,12 +16,15 @@ class OverlayPlugin(Plugin): 'green': (0, 1, 0), 'cyan': (0, 1, 1)} - def __init__(self, image_viewer, **kwargs): - Plugin.__init__(self, image_viewer, **kwargs) + def __init__(self, **kwargs): + super(OverlayPlugin, self).__init__(**kwargs) self._overlay_plot = None self._overlay = None self.cmap = None self.color_names = self.colors.keys() + + def attach(self, image_viewer): + super(OverlayPlugin, self).attach(image_viewer) #TODO: `color` doesn't update GUI widget when set manually. self.color = 0 diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index 38762dc4..66afa676 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -40,8 +40,9 @@ class PlotPlugin(Plugin): image : array Image used in measurement/manipulation. """ - def __init__(self, image_viewer, **kwargs): - Plugin.__init__(self, image_viewer, **kwargs) + + def attach(self, image_viewer): + super(PlotPlugin, self).attach(image_viewer) # Add plot for displaying intensity profile. self.add_plot() diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index ac004987..fe22d750 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -96,6 +96,10 @@ class ImageViewer(QtGui.QMainWindow): self.connect_event('motion_notify_event', self.update_status_bar) + def __iadd__(self, plugin): + plugin.attach(self) + return self + def closeEvent(self, ce): self.close() diff --git a/viewer_examples/plugins/canny.py b/viewer_examples/plugins/canny.py index e84a8270..eaa33330 100644 --- a/viewer_examples/plugins/canny.py +++ b/viewer_examples/plugins/canny.py @@ -5,5 +5,5 @@ from skimage.viewer.plugins.canny import CannyPlugin image = data.camera() viewer = ImageViewer(image) -CannyPlugin(viewer) +viewer += CannyPlugin() viewer.show() diff --git a/viewer_examples/plugins/lineprofile.py b/viewer_examples/plugins/lineprofile.py index 310c23e4..2f1b2cdc 100644 --- a/viewer_examples/plugins/lineprofile.py +++ b/viewer_examples/plugins/lineprofile.py @@ -5,5 +5,5 @@ from skimage.viewer.plugins.lineprofile import LineProfile image = data.camera() viewer = ImageViewer(image) -LineProfile(viewer) +viewer += LineProfile() viewer.show() From df18d402906266f9b762872eae0e674506aa3169 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 23 Jul 2012 00:18:32 -0400 Subject: [PATCH 103/648] Minor cleanup. --- skimage/viewer/plugins/base.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index ba68ecb6..7532472b 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -76,10 +76,7 @@ class Plugin(QtGui.QDialog): self.image_filter(*arguments, **kwargs) def _get_value(self, param): - if hasattr(param, 'val'): - return param.val() - else: - return param + return param if not hasattr(param, 'val') else param.val() def add_widget(self, widget): if widget.ptype == 'kwarg': From 977d17134df25d691fc3ee84b7366a48a60de677 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 23 Jul 2012 00:45:11 -0400 Subject: [PATCH 104/648] Change image_viewer to Plugin property. Raise an error when using Plugin.image_viewer before it is set. This error prevents other, more obscure, errors from getting raised. --- skimage/viewer/plugins/base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 7532472b..560eec8c 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -48,6 +48,16 @@ class Plugin(QtGui.QDialog): self.cids = [] self.artists = [] + @property + def image_viewer(self): + if self._image_viewer is None: + raise RuntimeError("Plugin is not attached to ImageViewer") + return self._image_viewer + + @image_viewer.setter + def image_viewer(self, image_viewer): + self._image_viewer = image_viewer + def attach(self, image_viewer): self.setParent(image_viewer) self.setWindowFlags(Qt.Dialog) From 92ca8374711b775364bd5b544b4c7c7a0d0e67a1 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 23 Jul 2012 01:13:03 -0400 Subject: [PATCH 105/648] ENH: Let Qt handle most of the window sizing. --- skimage/viewer/plugins/base.py | 9 +++++---- skimage/viewer/plugins/canny.py | 2 -- skimage/viewer/plugins/lineprofile.py | 3 +-- skimage/viewer/plugins/plotplugin.py | 1 + 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 560eec8c..5a626480 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -28,16 +28,17 @@ class Plugin(QtGui.QDialog): name = 'Plugin' draws_on_image = False - def __init__(self, image_filter=None, height=100, width=400, useblit=None): - QtGui.QDialog.__init__(self) + def __init__(self, image_filter=None, height=0, width=400, useblit=None): + super(Plugin, self).__init__() + self.image_viewer = None + if image_filter is not None: + self.image_filter = image_filter self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) self.resize(width, height) self.row = 0 - if image_filter is not None: - self.image_filter = image_filter self.arguments = [] self.keyword_arguments= {} diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 5ec6e415..b161fd87 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -9,8 +9,6 @@ class CannyPlugin(OverlayPlugin): name = 'Canny Filter' def __init__(self, *args, **kwargs): - kwargs.setdefault('height', 100) - kwargs.setdefault('width', 400) super(CannyPlugin, self).__init__(**kwargs) self.add_widget(Slider('sigma', 0, 5, update_on='release')) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index f41407b3..7b2e1c10 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -34,8 +34,7 @@ class LineProfile(PlotPlugin): draws_on_image = True def __init__(self, useblit=None, linewidth=1, epsilon=5, limits='image'): - super(LineProfile, self).__init__(height=200, width=600, - useblit=useblit) + super(LineProfile, self).__init__(useblit=useblit) self.linewidth = linewidth self.epsilon = epsilon self._active_pt = None diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index 66afa676..5c1ce7bb 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -23,6 +23,7 @@ class PlotCanvas(FigureCanvasQTAgg): FigureCanvasQTAgg.updateGeometry(self) # Note: `setParent` must be called after `FigureCanvasQTAgg.__init__`. self.setParent(parent) + self.setMinimumHeight(150) class PlotPlugin(Plugin): From 36b0fbd84e86c6bca62ce435d4c87906ca93b721 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 23 Jul 2012 01:22:05 -0400 Subject: [PATCH 106/648] Rename (dis)connect_event to (dis)connect_image_events. This clarifies action since these events are on the image viewer, not the plugin. --- skimage/viewer/plugins/base.py | 14 +++++++------- skimage/viewer/plugins/lineprofile.py | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 5a626480..fd7cb005 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -69,7 +69,7 @@ class Plugin(QtGui.QDialog): self.arguments.append(self.image_viewer.original_image) if self.draws_on_image: - self.connect_event('draw_event', self.on_draw) + self.connect_image_event('draw_event', self.on_draw) def on_draw(self, event): """Save image background when blitting. @@ -108,17 +108,17 @@ class Plugin(QtGui.QDialog): def closeEvent(self, event): """Disconnect all artists and events from ImageViewer. - Note that events must be connected using `self.connect_event` and + Note that events must be connected using `self.connect_image_event` and artists must be appended to `self.artists`. """ self.disconnect_image_events() - self.remove_artists() + self.remove_image_artists() self.image_viewer.plugins.remove(self) self.image_viewer.redraw() self.close() - def connect_event(self, event, callback): - """Connect callback with an event. + def connect_image_event(self, event, callback): + """Connect callback with an event in the image viewer. This should be used in lieu of `figure.canvas.mpl_connect` since this function stores call back ids for later clean up. @@ -138,7 +138,7 @@ class Plugin(QtGui.QDialog): for c in self.cids: self.image_viewer.disconnect_event(c) - def remove_artists(self): - """Disconnect artists that are connected to the *image plot*.""" + def remove_image_artists(self): + """Disconnect artists that are connected to the image viewer.""" for a in self.artists: self.image_viewer.remove_artist(a) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 7b2e1c10..5afe78f4 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -72,11 +72,11 @@ class LineProfile(PlotPlugin): self.profile = self.ax.plot(scan_data, 'k-')[0] self._autoscale_view() - self.connect_event('key_press_event', self.on_key_press) - self.connect_event('button_press_event', self.on_mouse_press) - self.connect_event('button_release_event', self.on_mouse_release) - self.connect_event('motion_notify_event', self.on_move) - self.connect_event('scroll_event', self.on_scroll) + self.connect_image_event('key_press_event', self.on_key_press) + self.connect_image_event('button_press_event', self.on_mouse_press) + self.connect_image_event('button_release_event', self.on_mouse_release) + self.connect_image_event('motion_notify_event', self.on_move) + self.connect_image_event('scroll_event', self.on_scroll) self.image_viewer.redraw() From daae405945adce516b080492fe9f0afce0287955 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 23 Jul 2012 21:55:30 -0400 Subject: [PATCH 107/648] DOC: Add todo note --- skimage/viewer/widgets/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 02d29545..e2086b4f 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -16,6 +16,8 @@ from PyQt4 import QtCore from skimage.io._plugins.q_color_mixer import IntelligentSlider +#TODO: Add WidgetBase class (requires reimplementation of IntelligentSlider). + class Slider(IntelligentSlider): """Slider widget. From 0e8f444fbb4ef53338289fa6372be5d4ed389993 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 00:28:37 -0400 Subject: [PATCH 108/648] ENH: Display overlay by default --- skimage/viewer/plugins/base.py | 10 +++++++++- skimage/viewer/plugins/canny.py | 6 +----- skimage/viewer/plugins/overlayplugin.py | 4 ++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index fd7cb005..d5db0f5c 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -84,7 +84,12 @@ class Plugin(QtGui.QDialog): arguments = [self._get_value(a) for a in self.arguments] kwargs = dict([(name, self._get_value(a)) for name, a in self.keyword_arguments.iteritems()]) - self.image_filter(*arguments, **kwargs) + filtered = self.image_filter(*arguments, **kwargs) + self.display_filtered_image(filtered) + + def display_filtered_image(self, image): + """Override this method to display image on image viewer.""" + pass def _get_value(self, param): return param if not hasattr(param, 'val') else param.val() @@ -102,6 +107,9 @@ class Plugin(QtGui.QDialog): self.layout.addWidget(widget, self.row, 0) self.row += 1 + def __iadd__(self, widget): + self.add_widget(widget) + def update_plugin(self, name, value): setattr(self, name, value) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index b161fd87..154fb6da 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -9,7 +9,7 @@ class CannyPlugin(OverlayPlugin): name = 'Canny Filter' def __init__(self, *args, **kwargs): - super(CannyPlugin, self).__init__(**kwargs) + super(CannyPlugin, self).__init__(image_filter=canny, **kwargs) self.add_widget(Slider('sigma', 0, 5, update_on='release')) self.add_widget(Slider('low threshold', 0, 255, update_on='release')) @@ -20,7 +20,3 @@ class CannyPlugin(OverlayPlugin): super(CannyPlugin, self).attach(image_viewer) # Update image overlay to default slider values. self.filter_image() - - def image_filter(self, *args, **kwargs): - image = canny(*args, **kwargs) - self.overlay = image diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 3b051e1a..3aa5a445 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -45,6 +45,10 @@ class OverlayPlugin(Plugin): self._overlay_plot.set_array(image) self.image_viewer.redraw() + def display_filtered_image(self, image): + """Display image over image in viewer.""" + self.overlay = image + def closeEvent(self, event): self.overlay = None super(OverlayPlugin, self).closeEvent(event) From 1ae662f712d20e1a2b50f21726214be96fe297ea Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 00:35:26 -0400 Subject: [PATCH 109/648] ENH: filter image when Plugin is attached to ImageViewer --- skimage/viewer/plugins/base.py | 7 +++++-- skimage/viewer/plugins/canny.py | 5 ----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index d5db0f5c..18768530 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -32,8 +32,7 @@ class Plugin(QtGui.QDialog): super(Plugin, self).__init__() self.image_viewer = None - if image_filter is not None: - self.image_filter = image_filter + self.image_filter = image_filter self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) @@ -70,6 +69,8 @@ class Plugin(QtGui.QDialog): if self.draws_on_image: self.connect_image_event('draw_event', self.on_draw) + # Call filter so that filtered image matches widget values + self.filter_image() def on_draw(self, event): """Save image background when blitting. @@ -81,6 +82,8 @@ class Plugin(QtGui.QDialog): self.img_background = self.image_viewer.canvas.copy_from_bbox(bbox) def filter_image(self, *args): + if self.image_filter is None: + return arguments = [self._get_value(a) for a in self.arguments] kwargs = dict([(name, self._get_value(a)) for name, a in self.keyword_arguments.iteritems()]) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 154fb6da..3431c6af 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -15,8 +15,3 @@ class CannyPlugin(OverlayPlugin): self.add_widget(Slider('low threshold', 0, 255, update_on='release')) self.add_widget(Slider('high threshold', 0, 255, update_on='release')) self.add_widget(ComboBox('color', self.color_names, ptype='plugin')) - - def attach(self, image_viewer): - super(CannyPlugin, self).attach(image_viewer) - # Update image overlay to default slider values. - self.filter_image() From 4b3f6d6c3016e5dee2ce893f96e1b494842f1e43 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 00:51:31 -0400 Subject: [PATCH 110/648] BUG: in-place add should return object Actually, I didn't mean to add `__iadd__` a couple of commits ago, so this was supposed to be an enhancement that allows you to access `add_widget` using in place adding. --- skimage/viewer/plugins/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 18768530..749d5741 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -92,7 +92,7 @@ class Plugin(QtGui.QDialog): def display_filtered_image(self, image): """Override this method to display image on image viewer.""" - pass + self.image_viewer.image = image def _get_value(self, param): return param if not hasattr(param, 'val') else param.val() @@ -112,6 +112,7 @@ class Plugin(QtGui.QDialog): def __iadd__(self, widget): self.add_widget(widget) + return self def update_plugin(self, name, value): setattr(self, name, value) From 6182cc9f1b58f96479a46dea03ffe98a6244e7e0 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 00:55:48 -0400 Subject: [PATCH 111/648] ENH: Add example of adding widgets to plugin --- viewer_examples/plugins/canny_simple.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 viewer_examples/plugins/canny_simple.py diff --git a/viewer_examples/plugins/canny_simple.py b/viewer_examples/plugins/canny_simple.py new file mode 100644 index 00000000..49bc15eb --- /dev/null +++ b/viewer_examples/plugins/canny_simple.py @@ -0,0 +1,20 @@ +from skimage import data +from skimage.filter import canny + +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import Slider +from skimage.viewer.plugins.overlayplugin import OverlayPlugin + + +image = data.camera() +# Note: ImageViewer must be called before Plugin b/c it starts the event loop. +viewer = ImageViewer(image) +# You can create a UI for a filter just by passing a filter function... +plugin = OverlayPlugin(image_filter=canny) +# ... and adding widgets to adjust parameter values. +plugin += Slider('sigma', 0, 5, update_on='release') +plugin += Slider('low threshold', 0, 255, update_on='release') +plugin += Slider('high threshold', 0, 255, update_on='release') +# Finally, attach the plugin to the image viewer. +viewer += plugin +viewer.show() From c1a859acae6c2c9d2525e159f1f471a673088de9 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 00:58:35 -0400 Subject: [PATCH 112/648] ENH: Change inplace-add to normal add to support alternate syntax Widgets can be added to Plugins inline, and Plugins can be added inline to Viewers. --- skimage/viewer/plugins/base.py | 2 +- skimage/viewer/viewers/core.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 749d5741..d9649b22 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -110,7 +110,7 @@ class Plugin(QtGui.QDialog): self.layout.addWidget(widget, self.row, 0) self.row += 1 - def __iadd__(self, widget): + def __add__(self, widget): self.add_widget(widget) return self diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index fe22d750..3fd9724e 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -96,7 +96,7 @@ class ImageViewer(QtGui.QMainWindow): self.connect_event('motion_notify_event', self.update_status_bar) - def __iadd__(self, plugin): + def __add__(self, plugin): plugin.attach(self) return self From c2d2919bae8a029910ab23fdc5a6c1e177d6ce9f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 01:31:47 -0400 Subject: [PATCH 113/648] BUG: Don't override `image_filter` method if defined by subclass --- skimage/viewer/plugins/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index d9649b22..cb76ffc3 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -32,7 +32,9 @@ class Plugin(QtGui.QDialog): super(Plugin, self).__init__() self.image_viewer = None - self.image_filter = image_filter + # If subclass defines `image_filter` method ignore input. + if not hasattr(self, 'image_filter'): + self.image_filter = image_filter self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) From f3024fc4cda567058dea1f72c5e7f0e9ecb95fe5 Mon Sep 17 00:00:00 2001 From: Brian Holt Date: Tue, 24 Jul 2012 10:52:55 +0100 Subject: [PATCH 114/648] simplified hog code + extra unit test --- skimage/feature/hog.py | 11 +++++------ skimage/feature/tests/test_hog.py | 21 ++++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/skimage/feature/hog.py b/skimage/feature/hog.py index 4a24b0a2..5206034e 100644 --- a/skimage/feature/hog.py +++ b/skimage/feature/hog.py @@ -107,6 +107,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), # compute orientations integral images orientation_histogram = np.zeros((n_cellsy, n_cellsx, orientations)) + subsample = np.index_exp[cy / 2:cy * n_cellsy:cy, cx / 2:cx * n_cellsx:cx] for i in range(orientations): #create new integral image for this orientation # isolate orientations in this range @@ -119,19 +120,17 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), cond2 = temp_ori > 0 temp_mag = np.where(cond2, magnitude, 0) - orientation_histogram[:, :, i] = uniform_filter(temp_mag, - size=(cy, cx))[cy / 2::cy, cx / 2::cx] + temp_filt = uniform_filter(temp_mag, size=(cy, cx)) + orientation_histogram[:, :, i] = temp_filt[subsample] # now for each cell, compute the histogram - #orientation_histogram = np.zeros((n_cellsx, n_cellsy, orientations)) - radius = min(cx, cy) // 2 - 1 hog_image = None - if visualise: - hog_image = np.zeros((sy, sx), dtype=float) if visualise: from skimage import draw + radius = min(cx, cy) // 2 - 1 + hog_image = np.zeros((sy, sx), dtype=float) for x in range(n_cellsx): for y in range(n_cellsy): for o in range(orientations): diff --git a/skimage/feature/tests/test_hog.py b/skimage/feature/tests/test_hog.py index 18c13c85..90e08105 100644 --- a/skimage/feature/tests/test_hog.py +++ b/skimage/feature/tests/test_hog.py @@ -1,18 +1,21 @@ -import numpy as np -import scipy - -from skimage.feature import hog - +from skimage import data +from skimage import feature +from skimage import img_as_float def test_histogram_of_oriented_gradients(): - # Replace with skimage.data.lena() after merge - img = scipy.misc.lena()[:256, :].astype(np.int8) + img = img_as_float(data.lena()[:256, :].mean(axis=2)) - fd = hog(img, orientations=9, pixels_per_cell=(8, 8), - cells_per_block=(1, 1)) + fd = feature.hog(img, orientations=9, pixels_per_cell=(8, 8), + cells_per_block=(1, 1)) assert len(fd) == 9 * (256 // 8) * (512 // 8) +def test_hog_image_size_cell_size_mismatch(): + image = data.camera()[:150, :200] + fd = feature.hog(image, orientations=9, pixels_per_cell=(8, 8), + cells_per_block=(1, 1)) + assert len(fd) == 9 * (150 // 8) * (200 // 8) + if __name__ == '__main__': from numpy.testing import run_module_suite run_module_suite() From aa92d5f0bd3d68144acecdaa1475acd74488a95e Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Tue, 24 Jul 2012 23:01:15 +0200 Subject: [PATCH 115/648] Better handling of labeled pixels in random walker segmentation when we return the whole probability. --- skimage/segmentation/random_walker_segmentation.py | 5 +++++ skimage/segmentation/tests/test_random_walker.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index a5bed48b..eaae60c9 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -350,6 +350,11 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, labels = labels.astype(np.float) X = np.array([_clean_labels_ar(Xline, labels, copy=True).reshape(data.shape) for Xline in X]) + for i in range(1, int(labels.max()) + 1): + mask_i = np.squeeze(labels == i) + X[i - 1, mask_i] = 1 + X[np.setdiff1d(np.arange(0, labels.max(), dtype=np.int), + [i - 1]), mask_i] = 0 else: X = _clean_labels_ar(X + 1, labels).reshape(data.shape) return X diff --git a/skimage/segmentation/tests/test_random_walker.py b/skimage/segmentation/tests/test_random_walker.py index aec9edea..6de312e7 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -58,7 +58,7 @@ def test_2d_bf(): return_full_prob=True) assert (full_prob_bf[1, 25:45, 40:60] >= full_prob_bf[0, 25:45, 40:60]).all() - return data, labels_bf + return data, labels_bf, full_prob_bf def test_2d_cg(): From ff98b059e71c35530a01dbc46b3cdc922c0b3292 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 23:06:53 -0400 Subject: [PATCH 116/648] STY: Remove unused `add_artist` method. --- skimage/viewer/viewers/core.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 3fd9724e..7b2b7f9a 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -1,3 +1,6 @@ +""" +ImageViewer class for viewing and interacting with images. +""" import sys from PyQt4 import QtGui, QtCore @@ -42,9 +45,16 @@ class ImageViewer(QtGui.QMainWindow): image : array Image being viewed. Setting this value will update the displayed frame. original_image : array - Plugins typically operate on (but don't change) the original image. + Plugins typically operate on (but don't change) the *original* image. plugins : list List of attached plugins. + + Examples + -------- + >>> viewer = ImageViewer(image) + >>> viewer += SomePlugin() + >>> viewer.show() + """ def __init__(self, image): # Start main loop @@ -97,10 +107,11 @@ class ImageViewer(QtGui.QMainWindow): self.connect_event('motion_notify_event', self.update_status_bar) def __add__(self, plugin): + """Add plugin to ImageViewer""" plugin.attach(self) return self - def closeEvent(self, ce): + def closeEvent(self, event): self.close() def auto_layout(self): @@ -109,12 +120,13 @@ class ImageViewer(QtGui.QMainWindow): self.move(0, 0) w = size.width() y = 0 - #TODO: Layout isn't correct for multiple plugins (overlaps). + #TODO: Layout isn't quite correct for multiple plugins (overlaps). for p in self.plugins: p.move(w, y) y += p.geometry().height() def show(self): + """Show ImageViewer and attached plugins.""" self.auto_layout() for p in self.plugins: p.show() @@ -143,12 +155,10 @@ class ImageViewer(QtGui.QMainWindow): """Disconnect callback by its id (returned by `connect_event`).""" self.canvas.mpl_disconnect(callback_id) - def add_artist(self, artist): - """Add matplotlib artist to image viewer.""" - self.ax.add_artist(artist) - def remove_artist(self, artist): - """Disconnect matplotlib artist from image viewer.""" + """Disconnect matplotlib artist from image viewer. + + """ # There's probably a smarter way to do this. for artist_list in self._axes_artists: if artist in artist_list: From 86526064f263c687a2134440d4db3a81cdb51bef Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 23:22:55 -0400 Subject: [PATCH 117/648] DOC: Improve docstrings for ImageViewer. Oops: also changed added leading underscore to `update_status_bar`. --- skimage/viewer/viewers/core.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 7b2b7f9a..1046ca7c 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -104,7 +104,7 @@ class ImageViewer(QtGui.QMainWindow): cs_size = self.canvas.sizeHint() self.resize(cs_size.width(), cs_size.height() + sb_size.height()) - self.connect_event('motion_notify_event', self.update_status_bar) + self.connect_event('motion_notify_event', self._update_status_bar) def __add__(self, plugin): """Add plugin to ImageViewer""" @@ -126,7 +126,10 @@ class ImageViewer(QtGui.QMainWindow): y += p.geometry().height() def show(self): - """Show ImageViewer and attached plugins.""" + """Show ImageViewer and attached plugins. + + This behaves much like `matplotlib.pyplot.show` and `QWidget.show`. + """ self.auto_layout() for p in self.plugins: p.show() @@ -158,13 +161,23 @@ class ImageViewer(QtGui.QMainWindow): def remove_artist(self, artist): """Disconnect matplotlib artist from image viewer. + The `closeEvent` method of a Plugin should remove artists (Matplotlib + lines, markers, etc.) from the viewer so that they aren't stranded. + + Parameters + ---------- + artist : Matplotlib Artist + Artists created by Matplotlib functions (e.g., `plot` returns list + of `Line2D` artists) should be saved by the plugin for removal. """ - # There's probably a smarter way to do this. + # Note: an `add_artist` method is unnecessary since Matplotlib + + # There's probably a smarter way to find where the artist is stored. for artist_list in self._axes_artists: if artist in artist_list: artist_list.remove(artist) - def update_status_bar(self, event): + def _update_status_bar(self, event): if event.inaxes and event.inaxes.get_navigate(): self.status_message(self._format_coord(event.xdata, event.ydata)) else: From a31e0d9eeb83f03d113c254bd12df9d9bcf721e9 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Tue, 24 Jul 2012 23:01:51 -0500 Subject: [PATCH 118/648] remove superfluous conditional logic --- skimage/io/_plugins/pil_plugin.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index f42efe89..2247f3a1 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -105,11 +105,7 @@ def imsave(fname, arr, format_str=None): arr = arr.astype(np.uint8) img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) - - if isinstance(fname, basestring): - img.save(fname, format=format_str) - elif callable(getattr(fname, 'write', None)): - img.save(fname, format=format_str) + img.save(fname, format=format_str) def imshow(arr): From 7368332df97f94b67b625a0837c4164122f879d4 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Tue, 24 Jul 2012 23:07:44 -0500 Subject: [PATCH 119/648] remove unused import --- skimage/io/_io.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 514b828c..29076433 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,8 +1,6 @@ __all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show', 'push', 'pop'] -import base64 - from skimage.io._plugins import call as call_plugin from skimage.color import rgb2grey import numpy as np From b9d468b6686eb40af29ad5c24b2dd6afa3e06542 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Tue, 24 Jul 2012 23:20:22 -0500 Subject: [PATCH 120/648] use standard conventions for Image.__new__() cls attribute and docstring --- skimage/io/_io.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 29076433..8320ef9f 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -22,20 +22,18 @@ class Image(np.ndarray): 'EXIF': {}, 'info': {}} - def __new__(image_cls, arr, **kwargs): + def __new__(cls, arr, **kwargs): """Set the image data and tags according to given parameters. Input: ------ - `image_cls` : Image class specification - This is not normally specified by the user. - `arr` : ndarray + arr : ndarray Image data. - ``**kwargs`` : Image tags as keywords + kwargs : Image tags as keywords Specified in the form ``tag0=value``, ``tag1=value``. """ - x = np.asarray(arr).view(image_cls) + x = np.asarray(arr).view(cls) for tag, value in Image.tags.items(): setattr(x, tag, kwargs.get(tag, getattr(arr, tag, value))) return x From 2b66f0c30314a8043efe0b8d5c9374b1468a3689 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Tue, 24 Jul 2012 23:21:42 -0500 Subject: [PATCH 121/648] role duplicate logic for Image._repr_png_() and Image._repr_jpeg_() into a common method --- skimage/io/_io.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 8320ef9f..95cb5abc 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -53,15 +53,14 @@ class Image(np.ndarray): return tuple(object_state) def _repr_png_(self): - str_buffer = StringIO.StringIO() - imsave(str_buffer, self, format_str='png') - return_str = str_buffer.getvalue() - str_buffer.close() - return return_str + return self._repr_image_format('png') def _repr_jpeg_(self): + return self._repr_image_format('jpeg') + + def _repr_image_format(self, format_str): str_buffer = StringIO.StringIO() - imsave(str_buffer, self, format_str='jpeg') + imsave(str_buffer, self, format_str=format_str) return_str = str_buffer.getvalue() str_buffer.close() return return_str From 51e61b3e46faa5ffd154a4ec8bc7bd7c4cabceeb Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 25 Jul 2012 00:21:56 -0400 Subject: [PATCH 122/648] DOC: Improve docstrings for Plugin class. --- skimage/viewer/plugins/base.py | 145 +++++++++++++++++++++++++-------- 1 file changed, 112 insertions(+), 33 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index cb76ffc3..2e7c8980 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,3 +1,6 @@ +""" +Base class for Plugins that interact with ImageViewer. +""" from PyQt4 import QtGui from PyQt4.QtCore import Qt @@ -5,25 +8,64 @@ import matplotlib as mpl class Plugin(QtGui.QDialog): - """Base class for widgets that interact with the axes. + """Base class for plugins that interact with an ImageViewer. + + A plugin connects an image filter (or another function) to an image viewer. + Note that a Plugin is initialized *without* an image viewer and attached in + a later step. See example below for details. Parameters ---------- - image_viewer : ImageViewer instance. + image_viewer : ImageViewer Window containing image used in measurement/manipulation. - callback : function - Function that gets called to update ImageViewer. Alternatively, this - can also be defined as a method in a Plugin subclass. + image_filter : function + Function that gets called to update image in image viewer. This value + can be `None` if, for example, you have a plugin that extracts + information from an image and doesn't manipulate it. Alternatively, + this function can be defined as a method in a Plugin subclass. height, width : int - Size of plugin window in pixels. + Size of plugin window in pixels. Note that Qt will automatically resize + a window to fit components. So if you're adding rows of components, you + can leave `height = 0` and just let Qt determine the final height. useblit : bool If True, use blitting to speed up animation. Only available on some - backends. If None, set to True when using Agg backend, otherwise False. + Matplotlib backends. If None, set to True when using Agg backend. + This only has an effect if you draw on top of an image viewer. Attributes ---------- image_viewer : ImageViewer Window containing image used in measurement. + name : str + Name of plugin. This is displayed as the window title. + artist : list + List of Matplotlib artists. Any artists created by the plugin should + be added to this list so that it gets cleaned up on close. + + Examples + -------- + >>> def my_func(image, arg1, arg2, optional_arg=0): + >>> ... + >>> + >>> viewer = ImageViewer(image) + >>> + >>> plugin = Plugin(image_filter=my_func) + >>> plugin += Widget('arg1', ..., ptype='arg') + >>> plugin += Widget('arg2', ..., ptype='arg') + >>> plugin += Widget('optional_arg', ..., ptype='kwarg') + >>> + >>> viewer.show() + + The plugin will automatically delegate parameters to `image_filter` based + on its parameter type, i.e., `ptype` (widgets for required arguments must + be added in the order they appear in the function). The image attached + to the viewer is **automatically passed as the first argument** to the + filter function. + + #TODO: Add flag so image is not passed to filter function by default. + + `ptype = 'kwarg'` is the default for most widgets so it's unnecessary here. + """ name = 'Plugin' draws_on_image = False @@ -61,6 +103,16 @@ class Plugin(QtGui.QDialog): self._image_viewer = image_viewer def attach(self, image_viewer): + """Attach the plugin to an ImageViewer. + + Note that the ImageViewer will automatically call this method when the + plugin is added to the ImageViewer. For example: + + >>> viewer += Plugin(...) + + Also note that `attach` automatically calls the filter function so that + the image matches the filtered value specified by attached widgets. + """ self.setParent(image_viewer) self.setWindowFlags(Qt.Dialog) @@ -74,32 +126,16 @@ class Plugin(QtGui.QDialog): # Call filter so that filtered image matches widget values self.filter_image() - def on_draw(self, event): - """Save image background when blitting. - - The saved image is used to "clear" the figure before redrawing artists. - """ - if self.useblit: - bbox = self.image_viewer.ax.bbox - self.img_background = self.image_viewer.canvas.copy_from_bbox(bbox) - - def filter_image(self, *args): - if self.image_filter is None: - return - arguments = [self._get_value(a) for a in self.arguments] - kwargs = dict([(name, self._get_value(a)) - for name, a in self.keyword_arguments.iteritems()]) - filtered = self.image_filter(*arguments, **kwargs) - self.display_filtered_image(filtered) - - def display_filtered_image(self, image): - """Override this method to display image on image viewer.""" - self.image_viewer.image = image - - def _get_value(self, param): - return param if not hasattr(param, 'val') else param.val() - def add_widget(self, widget): + """Add widget to plugin. + + Alternatively, Plugin's `__add__` method is overloaded to add widgets: + + >>> plugin += Widget(...) + + Widgets can adjust required or optional arguments of filter function or + parameters for the plugin. This is specified by the Widget's `ptype'. + """ if widget.ptype == 'kwarg': name = widget.name.replace(' ', '_') self.keyword_arguments[name] = widget @@ -116,11 +152,54 @@ class Plugin(QtGui.QDialog): self.add_widget(widget) return self + def on_draw(self, event): + """Save image background when blitting. + + The saved image is used to "clear" the figure before redrawing artists. + """ + if self.useblit: + bbox = self.image_viewer.ax.bbox + self.img_background = self.image_viewer.canvas.copy_from_bbox(bbox) + + def filter_image(self, *widget_arg): + """Call `image_filter` with widget args and kwargs + + Note: `display_filtered_image` is automatically called. + """ + # `widget_arg` is passed by the active widget but is unused since all + # filter arguments are pulled directly from attached the widgets. + + if self.image_filter is None: + return + arguments = [self._get_value(a) for a in self.arguments] + kwargs = dict([(name, self._get_value(a)) + for name, a in self.keyword_arguments.iteritems()]) + filtered = self.image_filter(*arguments, **kwargs) + self.display_filtered_image(filtered) + + def _get_value(self, param): + # If param is a widget, return its `val` attribute. + return param if not hasattr(param, 'val') else param.val() + + def display_filtered_image(self, image): + """Display the filtered image on image viewer. + + If you don't want to simply replace the displayed image with the + filtered image (e.g., you want to display a transparent overlay), + you can override this method. + """ + self.image_viewer.image = image + def update_plugin(self, name, value): + """Update keyword parameters of the plugin itself. + + These parameters will typically be implemented as class properties so + that they update the image or some other component. + """ setattr(self, name, value) def closeEvent(self, event): - """Disconnect all artists and events from ImageViewer. + """On close disconnect all artists and events from ImageViewer. Note that events must be connected using `self.connect_image_event` and artists must be appended to `self.artists`. From 260a336eb91edbfe0fe0e7ddb1c9809a1a0ac8b9 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 25 Jul 2012 00:31:08 -0400 Subject: [PATCH 123/648] ENH: allow color to be set by name --- skimage/viewer/plugins/overlayplugin.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 3aa5a445..1473ca51 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -5,11 +5,18 @@ from ..utils import ClearColormap class OverlayPlugin(Plugin): """Plugin for ImageViewer that displays an overlay on top of main image. + The base Plugin class displays the filtered image directly on the viewer. + OverlayPlugin will instead overlay an image with a transparent colormap. + + See base Plugin class for additional details. + Attributes ---------- overlay : array Overlay displayed on top of image. This overlay defaults to a color map with alpha values varying linearly from 0 to 1. + color : int + Color of overlay. """ colors = {'red': (1, 0, 0), 'yellow': (1, 1, 0), @@ -46,10 +53,11 @@ class OverlayPlugin(Plugin): self.image_viewer.redraw() def display_filtered_image(self, image): - """Display image over image in viewer.""" + """Display filtered image as an overlay on top of image in viewer.""" self.overlay = image def closeEvent(self, event): + # clear overlay from ImageViewer on close self.overlay = None super(OverlayPlugin, self).closeEvent(event) @@ -60,7 +68,10 @@ class OverlayPlugin(Plugin): @color.setter def color(self, index): # Update colormap whenever color is changed. - name = self.color_names[index] + if isinstance(index, basestring) and index not in self.color_names: + raise ValueError("%s not defined in OverlayPlugin.colors" % index) + else: + name = self.color_names[index] self._color = name rgb = self.colors[name] self.cmap = ClearColormap(rgb) From d72baa484f566507a0310a8ea3145498396d3d16 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 25 Jul 2012 00:32:26 -0400 Subject: [PATCH 124/648] STY: reorder methods for clarity. --- skimage/viewer/plugins/overlayplugin.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 1473ca51..dbf15e7d 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -52,15 +52,6 @@ class OverlayPlugin(Plugin): self._overlay_plot.set_array(image) self.image_viewer.redraw() - def display_filtered_image(self, image): - """Display filtered image as an overlay on top of image in viewer.""" - self.overlay = image - - def closeEvent(self, event): - # clear overlay from ImageViewer on close - self.overlay = None - super(OverlayPlugin, self).closeEvent(event) - @property def color(self): return self._color @@ -79,3 +70,12 @@ class OverlayPlugin(Plugin): if self._overlay_plot is not None: self._overlay_plot.set_cmap(self.cmap) self.image_viewer.redraw() + + def display_filtered_image(self, image): + """Display filtered image as an overlay on top of image in viewer.""" + self.overlay = image + + def closeEvent(self, event): + # clear overlay from ImageViewer on close + self.overlay = None + super(OverlayPlugin, self).closeEvent(event) From 49bdc3ae6f772de63c5992c8d45787ccca6779cd Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 25 Jul 2012 00:36:51 -0400 Subject: [PATCH 125/648] DOC: clean up docstring for PlotPlugin --- skimage/viewer/plugins/plotplugin.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index 5c1ce7bb..0c298381 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -27,19 +27,12 @@ class PlotCanvas(FigureCanvasQTAgg): class PlotPlugin(Plugin): - """Plugin for ImageViewer that contains a plot Canvas. + """Plugin for ImageViewer that contains a plot canvas. - Parameters - ---------- - image_viewer : ImageViewer instance. - Window containing image used in measurement/manipulation. + Base class for plugins that contain a Matplotlib plot canvas, which can, + for example, display an image histogram. - Attributes - ---------- - image_viewer : ImageViewer - Window containing image used in measurement. - image : array - Image used in measurement/manipulation. + See base Plugin class for additional details. """ def attach(self, image_viewer): From f261e76ef0c6131a7527ad6081422ac7d7f5d039 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 25 Jul 2012 00:40:36 -0400 Subject: [PATCH 126/648] DOC: cleanup docstring and reuse parameter defined by parent class --- skimage/viewer/plugins/lineprofile.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 5afe78f4..9dee09e2 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -11,13 +11,10 @@ __all__ = ['LineProfile'] class LineProfile(PlotPlugin): """Plugin to compute interpolated intensity under a scan line on an image. + See PlotPlugin and Plugin classes for additional details. + Parameters ---------- - image_viewer : ImageViewer instance. - Window containing image used in measurement. - useblit : bool - If True, use blitting to speed up animation. Only available on some - backends. If None, set to True when using Agg backend, otherwise False. linewidth : float Line width for interpolation. Wider lines average over more pixels. epsilon : float @@ -33,8 +30,8 @@ class LineProfile(PlotPlugin): name = 'Line Profile' draws_on_image = True - def __init__(self, useblit=None, linewidth=1, epsilon=5, limits='image'): - super(LineProfile, self).__init__(useblit=useblit) + def __init__(self, linewidth=1, epsilon=5, limits='image', **kwargs): + super(LineProfile, self).__init__(**kwargs) self.linewidth = linewidth self.epsilon = epsilon self._active_pt = None From 9285898d39b5e6de09041f480e0b8c82e612311a Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 25 Jul 2012 00:41:54 -0400 Subject: [PATCH 127/648] DOC: Add class docstring --- skimage/viewer/plugins/canny.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 3431c6af..7c7bfb3b 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -5,6 +5,7 @@ from ..widgets import Slider, ComboBox class CannyPlugin(OverlayPlugin): + """Canny filter plugin to show edges of an image.""" name = 'Canny Filter' From 03d037d0443a65af4ec73bfe5e217fa95af7477c Mon Sep 17 00:00:00 2001 From: Jonathan Helmus Date: Wed, 25 Jul 2012 11:11:25 -0400 Subject: [PATCH 128/648] ImageCollection now slices like other iterables --- skimage/io/collection.py | 47 +++++++++++++---------------- skimage/io/tests/test_collection.py | 10 ++++-- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index a78dbd14..9908c30d 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -258,14 +258,15 @@ class ImageCollection(object): Parameters ---------- n : int or slice - Slice selecting images for the new ImageCollection or the image - number to be returned. + The image number to be returned, or a slice selecting the images + and ordering to be returned in a new ImageCollection. Returns ------- - img : ImageCollection or ndarray - Imagecollection of the selected images or an ndarray if a single - image is specified. + img : ndarray or ImageCollection. + The `n`-th image in the collection, or a new ImageCollection with + the selected images. + """ if hasattr(n, '__index__'): n = n.__index__() @@ -283,29 +284,23 @@ class ImageCollection(object): self._cached = n return self.data[idx] - - else: # slice object was provided + else: + # A slice object was provided, so create a new ImageCollection + # object. Any loaded image data in the original ImageCollection + # will be copied by reference to the new object. Image data + # loaded after this creation is not linked. fidx = range(len(self.files))[n] - if len(fidx) == 1: # only one item requested - return self.__getitem__(fidx[0]) - else: - # create a new ImageCollection object, any loaded image data - # in the original ImageCollection will be copied by reference - # to the new object. Image data loaded after this creation - # are not linked. - fidx.sort() - new_ic = copy(self) - new_ic._files = [self.files[i] for i in fidx] - if self.conserve_memory: - if self._cached in fidx: - new_ic._cached = fidx[self._cached] - new_ic.data = np.copy(self.data) - else: - new_ic.data = np.empty(1, dtype=object) + new_ic = copy(self) + new_ic._files = [self.files[i] for i in fidx] + if self.conserve_memory: + if self._cached in fidx: + new_ic._cached = fidx.index(self._cached) + new_ic.data = np.copy(self.data) else: - new_ic.data = self.data[fidx] - - return new_ic + new_ic.data = np.empty(1, dtype=object) + else: + new_ic.data = self.data[fidx] + return new_ic def _check_imgnum(self, n): """Check that the given image number is valid.""" diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index 36e78e13..9db28ff7 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -44,10 +44,14 @@ class TestImageCollection(): assert_raises(IndexError, return_img, -num - 1) def test_slicing(self): - assert type(self.collection[:] is ImageCollection) + assert type(self.collection[:]) is ImageCollection assert len(self.collection[:]) == 2 - assert_array_almost_equal(self.collection[0], self.collection[:1]) - assert_array_almost_equal(self.collection[1], self.collection[1:]) + 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]) def test_files_property(self): assert isinstance(self.collection.files, list) From 3448e419b391e160a985e07718a0642c03e6c98f Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Thu, 26 Jul 2012 22:44:02 +0100 Subject: [PATCH 129/648] ENH slight cleanup, fixed *1000 bug, added test and rgb2lab convenience. --- skimage/color/colorconv.py | 119 +++++++++++++++----------- skimage/color/tests/test_colorconv.py | 35 +++++--- 2 files changed, 91 insertions(+), 63 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 352e3039..ebc18bfb 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -45,7 +45,7 @@ from __future__ import division __all__ = ['convert_colorspace', 'rgb2hsv', 'hsv2rgb', 'rgb2xyz', 'xyz2rgb', 'rgb2rgbcie', 'rgbcie2rgb', 'rgb2grey', 'rgb2gray', 'gray2rgb', - 'xyz2lab', 'lab2xyz', + 'xyz2lab', 'lab2xyz', 'lab2rgb', 'rgb2lab' ] __docformat__ = "restructuredtext en" @@ -547,21 +547,6 @@ def gray2rgb(image): return np.dstack((image, image, image)) -#---------------------- -# Constants for CIE LAB -#---------------------- -_one_third = 1.0 / 3.0 -_sixteen_hundred_sixteenth = 16.0 / 116.0 -# Observer= 2A, Illuminant= D65 -_xref = 0.95047 -_yref = 1. -_zref = 1.08883 -_inv_xref = 1.0 / _xref -_inv_yref = 1.0 / _yref -_inv_zref = 1.0 / _zref - - - #-------------------------------------------------------------- # The conversion functions that make use of the constants above #-------------------------------------------------------------- @@ -588,50 +573,47 @@ def xyz2lab(xyz): ----- Observer= 2A, Illuminant= D65 CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 - + References ---------- .. [1] http://www.easyrgb.com/index.php?X=MATH&H=07#text7 .. [2] http://en.wikipedia.org/wiki/Lab_color_space - + Examples -------- >>> import os - >>> from skimage import data_dir + >>> from skimage import data_dir >>> from skimage.color import rgb2xyz, xyz2lab >>> from skimage.io import imread >>> lena = imread(os.path.join(data_dir, 'lena.png')) >>> lena_xyz = rgb2xyz(lena) >>> lena_lab = xyz2lab(lena_xyz) """ - arr = _prepare_colorarray(xyz).copy() - out = np.empty_like(arr) + arr = _prepare_colorarray(xyz) + + #---------------------- + # Constants for CIE LAB + #---------------------- + # Observer= 2A, Illuminant= D65 + ref_white = np.array([0.95047, 1., 1.08883]) # scale by CIE XYZ tristimulus values of the reference white point - x, y, z = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] - x *= _inv_xref - y *= _inv_yref - z *= _inv_zref + arr = arr / ref_white # Nonlinear distortion and linear transformation mask = arr > 0.008856 - arr[mask] = np.power(arr[mask], _one_third) - arr[~mask] = 7.787 * arr[~mask] + _sixteen_hundred_sixteenth - + arr[mask] = np.power(arr[mask], 1. / 3.) + arr[~mask] = 7.787 * arr[~mask] + 16. / 116. + + x, y, z = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] + # Vector scaling L = (116. * y) - 16. a = 500.0 * (x - y) b = 200.0 * (y - z) - # -- output - out[:, :, 0] = L - out[:, :, 1] = a - out[:, :, 2] = b + return np.dstack([L, a, b]) - # remove NaN - out[np.isnan(out)] = 0 - - return out def lab2xyz(lab): """CIE-LAB to XYZcolor space conversion. @@ -664,30 +646,69 @@ def lab2xyz(lab): """ arr = _prepare_colorarray(lab).copy() - out = np.empty_like(arr) L, a, b = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] y = (L + 16.) / 116. x = (a / 500.) + y z = y - (b / 200.) - out[:, :, 0] = x - out[:, :, 1] = y - out[:, :, 2] = z + out = np.dstack([x, y, z]) mask = out > 0.2068966 out[mask] = np.power(out[mask], 3.) - out[~mask] = (out[~mask] - _sixteen_hundred_sixteenth) / 7.787*1000 + out[~mask] = (out[~mask] - 16.0 / 116.) / 7.787 # rescale Observer= 2 deg, Illuminant= D65 - #x, y, z = out[:, :, 0], out[:, :, 1], out[:, :, 2] - out[:, :, 0] *= _xref - out[:, :, 1] *= _yref - out[:, :, 2] *= _zref - - # remove NaN - out[np.isnan(out)] = 0 - + ref_white = np.array([0.95047, 1., 1.08883]) + out *= ref_white return out +def rgb2lab(rgb): + """RGB to lab color space conversion. + + Parameters + ---------- + rgb : array_like + The image in RGB format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in Lab format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `rgb` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + This function uses rgb2xyz and xyz2lab. + """ + return xyz2lab(rgb2xyz(rgb)) + + +def lab2rgb(lab): + """Lab to RGB color space conversion. + + Parameters + ---------- + rgb : array_like + The image in Lab format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in RGB format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `lab` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + This function uses lab2xyz and xyz2rgb. + """ + return xyz2rgb(lab2xyz(lab)) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 7be4f454..316d801a 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -25,6 +25,7 @@ from skimage.color import ( convert_colorspace, rgb2grey, gray2rgb, xyz2lab, lab2xyz, + lab2rgb, rgb2lab ) from skimage import data_dir @@ -44,18 +45,17 @@ class TestColorconv(TestCase): colbars_point75 = colbars * 0.75 colbars_point75_array = np.swapaxes(colbars_point75.reshape(3, 4, 2), 0, 2) - xyz_array = np.array([[[0.4124, 0.21260, 0.01930]], #red - [[0, 0, 0]], #black - [[.9505, 1., 1.089]], #white - [[.1805, .0722, .9505]], #blue - [[.07719, .15438, .02573]], #green + xyz_array = np.array([[[0.4124, 0.21260, 0.01930]], # red + [[0, 0, 0]], # black + [[.9505, 1., 1.089]], # white + [[.1805, .0722, .9505]], # blue + [[.07719, .15438, .02573]], # green ]) - - lab_array = np.array([[[53.233, 80.109, 67.220]], #red - [[0.,0.,0.]], #black - [[100.0, 0.005, -0.010]], #white - [[32.303, 79.197, -107.864]], #blue - [[46.229, -51.7, 49.898]], #green + lab_array = np.array([[[53.233, 80.109, 67.220]], # red + [[0., 0., 0.]], # black + [[100.0, 0.005, -0.010]], # white + [[32.303, 79.197, -107.864]], # blue + [[46.229, -51.7, 49.898]], # green ]) # RGB to HSV @@ -167,10 +167,17 @@ class TestColorconv(TestCase): # test matrices for xyz2lab and lab2xyz generated using http://www.easyrgb.com/index.php?X=CALC # Note: easyrgb website displays xyz*100 def test_xyz2lab(self): - assert_array_almost_equal(xyz2lab(self.xyz_array), self.lab_array, decimal=3) + assert_array_almost_equal(xyz2lab(self.xyz_array), + self.lab_array, decimal=3) + + def test_lab2xyz(self): + assert_array_almost_equal(lab2xyz(self.lab_array), + self.xyz_array, decimal=3) + + def test_lab_rgb_roundtrip(self): + img_rgb = img_as_float(self.img_rgb) + assert_array_almost_equal(lab2rgb(rgb2lab(img_rgb)), img_rgb) - def test_lab2xyz(self): - assert_array_almost_equal(lab2xyz(self.lab_array), self.xyz_array, decimal=3) def test_gray2rgb(): x = np.array([0, 0.5, 1]) From 539b12dc2ca6240db78acda29d257de477369213 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 27 Jul 2012 21:57:46 -0400 Subject: [PATCH 130/648] STY: Refactor MatplotlibCanvas from ImageCanvas and PlotCanvas. --- skimage/viewer/plugins/plotplugin.py | 14 +++----------- skimage/viewer/utils/core.py | 17 ++++++++++++++++- skimage/viewer/viewers/core.py | 14 +++----------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index 0c298381..fa06088d 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -2,12 +2,12 @@ import numpy as np from PyQt4 import QtGui import matplotlib.pyplot as plt -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg +from ..utils import MatplotlibCanvas from .base import Plugin -class PlotCanvas(FigureCanvasQTAgg): +class PlotCanvas(MatplotlibCanvas): """Canvas for displaying images. This canvas derives from Matplotlib, and has attributes `fig` and `ax`, @@ -15,17 +15,9 @@ class PlotCanvas(FigureCanvasQTAgg): """ def __init__(self, parent, height, width, **kwargs): self.fig, self.ax = plt.subplots(figsize=(height, width), **kwargs) - - FigureCanvasQTAgg.__init__(self, self.fig) - FigureCanvasQTAgg.setSizePolicy(self, - QtGui.QSizePolicy.Expanding, - QtGui.QSizePolicy.Expanding) - FigureCanvasQTAgg.updateGeometry(self) - # Note: `setParent` must be called after `FigureCanvasQTAgg.__init__`. - self.setParent(parent) + super(PlotCanvas, self).__init__(parent, self.fig, **kwargs) self.setMinimumHeight(150) - class PlotPlugin(Plugin): """Plugin for ImageViewer that contains a plot canvas. diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index cdcc10bf..81d66183 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -1,9 +1,11 @@ import numpy as np import matplotlib.pyplot as plt from matplotlib.colors import LinearSegmentedColormap +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg +from PyQt4 import QtGui -__all__ = ['figimage', 'LinearColormap', 'ClearColormap'] +__all__ = ['figimage', 'LinearColormap', 'ClearColormap', 'MatplotlibCanvas'] def figimage(image, scale=1, dpi=None, **kwargs): @@ -72,3 +74,16 @@ class ClearColormap(LinearColormap): 'red': [(0.0, r), (1.0, r)], 'alpha': [(0.0, 0.0), (1.0, max_alpha)]} LinearColormap.__init__(self, name, cg_speq) + + +class MatplotlibCanvas(FigureCanvasQTAgg): + """Canvas for displaying images.""" + def __init__(self, parent, figure, **kwargs): + self.fig = figure + FigureCanvasQTAgg.__init__(self, self.fig) + FigureCanvasQTAgg.setSizePolicy(self, + QtGui.QSizePolicy.Expanding, + QtGui.QSizePolicy.Expanding) + FigureCanvasQTAgg.updateGeometry(self) + # Note: `setParent` must be called after `FigureCanvasQTAgg.__init__`. + self.setParent(parent) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 1046ca7c..dc38b422 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -4,26 +4,18 @@ ImageViewer class for viewing and interacting with images. import sys from PyQt4 import QtGui, QtCore -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg -from skimage.viewer.utils import figimage +from ..utils import figimage, MatplotlibCanvas qApp = None -class ImageCanvas(FigureCanvasQTAgg): +class ImageCanvas(MatplotlibCanvas): """Canvas for displaying images.""" def __init__(self, parent, image, **kwargs): self.fig, self.ax = figimage(image, **kwargs) - - FigureCanvasQTAgg.__init__(self, self.fig) - FigureCanvasQTAgg.setSizePolicy(self, - QtGui.QSizePolicy.Expanding, - QtGui.QSizePolicy.Expanding) - FigureCanvasQTAgg.updateGeometry(self) - # Note: `setParent` must be called after `FigureCanvasQTAgg.__init__`. - self.setParent(parent) + super(ImageCanvas, self).__init__(parent, self.fig, **kwargs) class ImageViewer(QtGui.QMainWindow): From 4620ee734edb0cc13fef239224a70990ba32fc5c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 27 Jul 2012 22:21:26 -0400 Subject: [PATCH 131/648] ENH: Create new Slider with editbox. Also, make the behavior more consistent between updating plugin and widget parameters. --- skimage/viewer/plugins/base.py | 2 +- skimage/viewer/widgets/core.py | 118 +++++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 2e7c8980..ee2cbb5d 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -179,7 +179,7 @@ class Plugin(QtGui.QDialog): def _get_value(self, param): # If param is a widget, return its `val` attribute. - return param if not hasattr(param, 'val') else param.val() + return param if not hasattr(param, 'val') else param.val def display_filtered_image(self, image): """Display the filtered image on image viewer. diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index e2086b4f..6e48197e 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -11,16 +11,28 @@ specified by its `ptype` attribute, which can be: a class property that updates the display. """ +from PyQt4.QtCore import Qt from PyQt4 import QtGui from PyQt4 import QtCore -from skimage.io._plugins.q_color_mixer import IntelligentSlider + + +__all__ = ['Slider', 'ComboBox'] #TODO: Add WidgetBase class (requires reimplementation of IntelligentSlider). -class Slider(IntelligentSlider): +class Slider(QtGui.QWidget): """Slider widget. + 'name' attribute and calls a callback + with 'name' as an argument to the registered callback. + + This allows you to create large groups of sliders in a loop, + but still keep track of the individual events + + It also prints a label below the slider. + + Parameters ---------- name : str @@ -30,14 +42,108 @@ class Slider(IntelligentSlider): name of the slider. low, high : float Range of slider values. + value : float + Default slider value. If None, use midpoint between `low` and `high`. ptype : {'arg' | 'kwarg' | 'plugin'} Parameter type. + callback : function + Callback function called in response to slider changes. + orientation : {'horizontal' | 'vertical'} + Slider orientation. + update_on : {'move' | 'release'} + Control when callback function is called: on slider move or release. """ - def __init__(self, name, low, high, ptype='kwarg', callback=None, **kwargs): + def __init__(self, name, low=0.0, high=1.0, value=None, ptype='kwarg', + callback=None, max_edit_width=60, orientation='horizontal', + update_on='move'): + super(Slider, self).__init__() + self.name = name self.ptype = ptype - kwargs.setdefault('orientation', 'horizontal') - scale = (high - low) / 1000.0 - super(Slider, self).__init__(name, scale, low, callback, **kwargs) + self.callback = callback + + # divide slider into 1000 discrete values + slider_min = 0 + slider_max = 1000 + if value is None: + value = 500 + scale = float(high - low) / slider_max + self._scale = scale + self._low = low + self._high = high + + if orientation == 'vertical': + orientation_slider = Qt.Vertical + alignment = QtCore.Qt.AlignHCenter + align_text = QtCore.Qt.AlignHCenter + align_value = QtCore.Qt.AlignHCenter + self.layout = QtGui.QVBoxLayout(self) + elif orientation == 'horizontal': + orientation_slider = Qt.Horizontal + alignment = QtCore.Qt.AlignVCenter + align_text = QtCore.Qt.AlignLeft + align_value = QtCore.Qt.AlignRight + self.layout = QtGui.QHBoxLayout(self) + else: + msg = "Unexpected value %s for 'orientation'" + raise ValueError(msg % orientation) + + self.slider = QtGui.QSlider(orientation_slider) + self.slider.setRange(slider_min, slider_max) + self.slider.setValue(value) + if update_on == 'move': + self.slider.valueChanged.connect(self._on_slider_changed) + elif update_on == 'release': + self.slider.sliderReleased.connect(self._on_slider_changed) + else: + raise ValueError("Unexpected value %s for 'update_on'" % update_on) + + self.name_label = QtGui.QLabel() + self.name_label.setText(self.name) + self.name_label.setAlignment(align_text) + + self.editbox = QtGui.QLineEdit() + self.editbox.setMaximumWidth(max_edit_width) + self.editbox.setText('%2.2f' % self.val) + self.editbox.setAlignment(align_value) + self.editbox.editingFinished.connect(self._on_editbox_changed) + + self.layout.addWidget(self.name_label, alignment=align_text) + self.layout.addWidget(self.slider, alignment=alignment) + self.layout.addWidget(self.editbox, alignment=align_value) + + def _on_slider_changed(self): + """Call callback function with slider's name and value as parameters""" + value = self.val + self.editbox.setText(str(value)[:4]) + self.callback(self.name, value) + + def _on_editbox_changed(self): + """Validate input and set slider value""" + try: + value = float(self.editbox.text()) + except ValueError: + self._bad_editbox_input() + return + if not self._low <= value <= self._high: + self._bad_editbox_input() + return + + slider_value = (value - self._low) / self._scale + self.slider.setValue(slider_value) + self._good_editbox_input() + + def _good_editbox_input(self): + self.editbox.setStyleSheet("background-color: rgb(255, 255, 255)") + + def _bad_editbox_input(self): + self.editbox.setStyleSheet("background-color: rgb(255, 200, 200)") + + @property + def val(self): + return self.slider.value() * self._scale + self._low + + def _value_changed(self, value): + self.callback(self.name, value) class ComboBox(QtGui.QWidget): From 117d13a800bea7291a4cba53a01405a6fb6129c1 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 27 Jul 2012 22:27:24 -0400 Subject: [PATCH 132/648] Revert modifications of IntelligentSlider. Slider added in last commit removes the need for these modifications. --- skimage/io/_plugins/q_color_mixer.py | 46 ++++++---------------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/skimage/io/_plugins/q_color_mixer.py b/skimage/io/_plugins/q_color_mixer.py index 7a5d1778..09e8709f 100644 --- a/skimage/io/_plugins/q_color_mixer.py +++ b/skimage/io/_plugins/q_color_mixer.py @@ -8,7 +8,7 @@ from util import ColorMixer class IntelligentSlider(QWidget): ''' A slider that adds a 'name' attribute and calls a callback - with 'name' as an argument to the registered callback. + with 'name' as an argument to the registerd callback. This allows you to create large groups of sliders in a loop, but still keep track of the individual events @@ -18,8 +18,7 @@ class IntelligentSlider(QWidget): The range of the slider is hardcoded from zero - 1000, but it supports a conversion factor so you can scale the results''' - def __init__(self, name, a, b, callback, orientation='vertical', - update_on='move'): + def __init__(self, name, a, b, callback): QWidget.__init__(self) self.name = name self.callback = callback @@ -27,52 +26,27 @@ class IntelligentSlider(QWidget): self.b = b self.manually_triggered = False - if orientation == 'vertical': - orientation_slider = Qt.Vertical - alignment = QtCore.Qt.AlignHCenter - align_text = QtCore.Qt.AlignCenter - align_value = QtCore.Qt.AlignCenter - elif orientation == 'horizontal': - orientation_slider = Qt.Horizontal - alignment = QtCore.Qt.AlignVCenter - align_text = QtCore.Qt.AlignLeft - align_value = QtCore.Qt.AlignRight - - self.slider = QSlider(orientation_slider) + self.slider = QSlider() self.slider.setRange(0, 1000) self.slider.setValue(500) - if update_on == 'move': - self.slider.sliderMoved.connect(self.slider_changed) - elif update_on == 'release': - self.slider.sliderReleased.connect(self.slider_changed) - else: - raise ValueError("Unexpected value %s for 'update_on'" % update_on) + self.slider.valueChanged.connect(self.slider_changed) self.name_label = QLabel() self.name_label.setText(self.name) - self.name_label.setAlignment(align_text) + self.name_label.setAlignment(QtCore.Qt.AlignCenter) self.value_label = QLabel() self.value_label.setText('%2.2f' % (self.slider.value() * self.a + self.b)) - self.value_label.setAlignment(align_value) + self.value_label.setAlignment(QtCore.Qt.AlignCenter) self.layout = QGridLayout(self) - - if orientation == 'vertical': - self.layout.addWidget(self.name_label, 0, 0) - self.layout.addWidget(self.slider, 1, 0, alignment) - self.layout.addWidget(self.value_label, 2, 0) - elif orientation == 'horizontal': - self.layout.addWidget(self.name_label, 0, 0) - self.layout.addWidget(self.slider, 0, 1, alignment) - self.layout.addWidget(self.value_label, 0, 2) - else: - msg = "Unexpected value %s for 'orientation'" - raise ValueError(msg % orientation) + self.layout.addWidget(self.name_label, 0, 0) + self.layout.addWidget(self.slider, 1, 0, QtCore.Qt.AlignHCenter) + self.layout.addWidget(self.value_label, 2, 0) # bind this to the valueChanged signal of the slider - def slider_changed(self): + def slider_changed(self, val): val = self.val() self.value_label.setText(str(val)[:4]) From 7615a856c5308ea9743c98457ca1d78b293d6b46 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 27 Jul 2012 22:32:42 -0400 Subject: [PATCH 133/648] Remove lineprofile temporarily (saved in a branch) --- skimage/viewer/plugins/lineprofile.py | 240 ------------------------- skimage/viewer/plugins/plotplugin.py | 50 ------ viewer_examples/plugins/lineprofile.py | 9 - 3 files changed, 299 deletions(-) delete mode 100644 skimage/viewer/plugins/lineprofile.py delete mode 100644 skimage/viewer/plugins/plotplugin.py delete mode 100644 viewer_examples/plugins/lineprofile.py diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py deleted file mode 100644 index 9dee09e2..00000000 --- a/skimage/viewer/plugins/lineprofile.py +++ /dev/null @@ -1,240 +0,0 @@ -import numpy as np -import scipy.ndimage as ndi -from skimage.util.dtype import dtype_range - -from .plotplugin import PlotPlugin - - -__all__ = ['LineProfile'] - - -class LineProfile(PlotPlugin): - """Plugin to compute interpolated intensity under a scan line on an image. - - See PlotPlugin and Plugin classes for additional details. - - Parameters - ---------- - linewidth : float - Line width for interpolation. Wider lines average over more pixels. - epsilon : float - Maximum pixel distance allowed when selecting end point of scan line. - limits : tuple or {None, 'image', 'dtype'} - (minimum, maximum) intensity limits for plotted profile. The following - special values are defined: - - None : rescale based on min/max intensity along selected scan line. - 'image' : fixed scale based on min/max intensity in image. - 'dtype' : fixed scale based on min/max intensity of image dtype. - """ - name = 'Line Profile' - draws_on_image = True - - def __init__(self, linewidth=1, epsilon=5, limits='image', **kwargs): - super(LineProfile, self).__init__(**kwargs) - self.linewidth = linewidth - self.epsilon = epsilon - self._active_pt = None - self._limit_type = limits - self.line_kwargs = dict(color='y', lw=linewidth, alpha=0.5, marker='s', - markersize=5, solid_capstyle='butt') - print self.help() - - def attach(self, image_viewer): - super(LineProfile, self).attach(image_viewer) - - image = image_viewer.original_image - - if self._limit_type == 'image': - self.limits = (np.min(image), np.max(image)) - elif self._limit_type == 'dtype': - self.self._limit_type = dtype_range[image.dtype.type] - elif self._limit_type is None or len(self._limit_type) == 2: - self.limits = self._limit_type - else: - raise ValueError("Unrecognized `limits`: %s" % self._limit_type) - - if not self._limit_type is None: - self.ax.set_ylim(self.limits) - - h, w = image.shape - self._init_end_pts = np.array([[w/3, h/2], [2*w/3, h/2]]) - self.end_pts = self._init_end_pts.copy() - - x, y = np.transpose(self.end_pts) - self.scan_line = image_viewer.ax.plot(x, y, **self.line_kwargs)[0] - self.artists.append(self.scan_line) - - scan_data = profile_line(image, self.end_pts) - self.profile = self.ax.plot(scan_data, 'k-')[0] - self._autoscale_view() - - self.connect_image_event('key_press_event', self.on_key_press) - self.connect_image_event('button_press_event', self.on_mouse_press) - self.connect_image_event('button_release_event', self.on_mouse_release) - self.connect_image_event('motion_notify_event', self.on_move) - self.connect_image_event('scroll_event', self.on_scroll) - - self.image_viewer.redraw() - - def help(self): - helpstr = ("Line profile tool", - "+ and - keys or mouse scroll changes width of scan line.", - "Select and drag ends of the scan line to adjust it.") - return '\n'.join(helpstr) - - def get_profile(self): - """Return intensity profile of the selected line. - - Returns - ------- - end_pts: (2, 2) array - The positions ((x1, y1), (x2, y2)) of the line ends. - profile: 1d array - Profile of intensity values. - """ - end_pts = self.scan_line.get_xydata() - profile = self.profile.get_ydata() - return end_pts, profile - - def on_scroll(self, event): - if not event.inaxes: return - if event.button == 'up': - self._thicken_scan_line() - elif event.button == 'down': - self._shrink_scan_line() - - def on_key_press(self, event): - if not event.inaxes: return - elif event.key == '+': - self._thicken_scan_line() - elif event.key == '-': - self._shrink_scan_line() - elif event.key == 'r': - self.reset() - - def _thicken_scan_line(self): - self.linewidth += 1 - self.line_changed(None, None) - - def _shrink_scan_line(self): - if self.linewidth > 1: - self.linewidth -= 1 - self.line_changed(None, None) - - def _autoscale_view(self): - if self.limits is None: - self.ax.autoscale_view(tight=True) - else: - self.ax.autoscale_view(scaley=False, tight=True) - - def get_pt_under_cursor(self, event): - """Return index of the end point under cursor, if sufficiently close""" - xy = np.asarray(self.scan_line.get_xydata()) - xyt = self.scan_line.get_transform().transform(xy) - xt, yt = xyt[:, 0], xyt[:, 1] - d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2) - indseq = np.nonzero(np.equal(d, np.amin(d)))[0] - ind = indseq[0] - if d[ind] >= self.epsilon: - ind = None - return ind - - def on_mouse_press(self, event): - if event.button != 1: return - if event.inaxes==None: return - self._active_pt = self.get_pt_under_cursor(event) - - def on_mouse_release(self, event): - if event.button != 1: return - self._active_pt = None - - def on_move(self, event): - if event.button != 1: return - if self._active_pt is None: return - if not self.image_viewer.ax.in_axes(event): return - x,y = event.xdata, event.ydata - self.line_changed(x, y) - - def reset(self): - self.end_pts = self._init_end_pts.copy() - self.scan_line.set_data(np.transpose(self.end_pts)) - self.line_changed(None, None) - - def line_changed(self, x, y): - if x is not None: - self.end_pts[self._active_pt, :] = x, y - self.scan_line.set_data(np.transpose(self.end_pts)) - self.scan_line.set_linewidth(self.linewidth) - - scan = profile_line(self.image_viewer.original_image, self.end_pts, - linewidth=self.linewidth) - self.profile.set_xdata(np.arange(scan.shape[0])) - self.profile.set_ydata(scan) - - self.ax.relim() - - if self.useblit: - self.image_viewer.canvas.restore_region(self.img_background) - self.ax.draw_artist(self.scan_line) - self.ax.draw_artist(self.profile) - self.image_viewer.canvas.blit(self.image_viewer.ax.bbox) - - self._autoscale_view() - - self.image_viewer.redraw() - self.redraw() - - -def profile_line(img, end_pts, linewidth=1): - """Return the intensity profile of an image measured along a scan line. - - Parameters - ---------- - img : 2d array - The image. - end_pts: (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_pts - x1, y1 = point1 = np.asarray(point1, dtype = float) - x2, y2 = point2 = np.asarray(point2, dtype = float) - dx, dy = point2 - point1 - - # Quick calculation if perfectly horizontal or vertical (remove?) - if x1 == x2: - pixels = img[min(y1, y2) : max(y1, y2)+1, - x1 - linewidth / 2 : x1 + linewidth / 2 + 1] - intensities = pixels.mean(axis = 1) - return intensities - elif y1 == y2: - pixels = img[y1 - linewidth / 2 : y1 + linewidth / 2 + 1, - min(x1, x2) : max(x1, x2)+1] - intensities = pixels.mean(axis = 0) - return intensities - - theta = np.arctan2(dy,dx) - a = dy/dx - b = y1 - a * x1 - length = np.hypot(dx, dy) - - line_x = np.linspace(min(x1, x2), max(x1, x2), 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]) - pixels = ndi.map_coordinates(img, perp_lines) - intensities = pixels.mean(axis=1) - - return intensities diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py deleted file mode 100644 index fa06088d..00000000 --- a/skimage/viewer/plugins/plotplugin.py +++ /dev/null @@ -1,50 +0,0 @@ -import numpy as np -from PyQt4 import QtGui - -import matplotlib.pyplot as plt - -from ..utils import MatplotlibCanvas -from .base import Plugin - - -class PlotCanvas(MatplotlibCanvas): - """Canvas for displaying images. - - This canvas derives from Matplotlib, and has attributes `fig` and `ax`, - which point to Matplotlib figure and axes. - """ - def __init__(self, parent, height, width, **kwargs): - self.fig, self.ax = plt.subplots(figsize=(height, width), **kwargs) - super(PlotCanvas, self).__init__(parent, self.fig, **kwargs) - self.setMinimumHeight(150) - -class PlotPlugin(Plugin): - """Plugin for ImageViewer that contains a plot canvas. - - Base class for plugins that contain a Matplotlib plot canvas, which can, - for example, display an image histogram. - - See base Plugin class for additional details. - """ - - def attach(self, image_viewer): - super(PlotPlugin, self).attach(image_viewer) - # Add plot for displaying intensity profile. - self.add_plot() - - def redraw(self): - """Redraw plot.""" - self.canvas.draw_idle() - - def add_plot(self, height=4, width=4): - self.canvas = PlotCanvas(self, height, width) - self.fig = self.canvas.fig - #TODO: Converted color is slightly different than Qt background. - qpalette = QtGui.QPalette() - qcolor = qpalette.color(QtGui.QPalette.Window) - bgcolor = qcolor.toRgb().value() - if np.isscalar(bgcolor): - bgcolor = str(bgcolor / 255.) - self.fig.patch.set_facecolor(bgcolor) - self.ax = self.canvas.ax - self.layout.addWidget(self.canvas, self.row, 0) diff --git a/viewer_examples/plugins/lineprofile.py b/viewer_examples/plugins/lineprofile.py deleted file mode 100644 index 2f1b2cdc..00000000 --- a/viewer_examples/plugins/lineprofile.py +++ /dev/null @@ -1,9 +0,0 @@ -from skimage import data -from skimage.viewer import ImageViewer -from skimage.viewer.plugins.lineprofile import LineProfile - - -image = data.camera() -viewer = ImageViewer(image) -viewer += LineProfile() -viewer.show() From 129070596db85160bc287126ba897e3ba05ad3fe Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 27 Jul 2012 22:36:33 -0400 Subject: [PATCH 134/648] Remove unused import --- skimage/io/_plugins/q_color_mixer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/io/_plugins/q_color_mixer.py b/skimage/io/_plugins/q_color_mixer.py index 09e8709f..3fe9e29c 100644 --- a/skimage/io/_plugins/q_color_mixer.py +++ b/skimage/io/_plugins/q_color_mixer.py @@ -1,7 +1,6 @@ # the module for the qt color_mixer plugin from PyQt4 import QtGui, QtCore from PyQt4.QtGui import (QWidget, QStackedWidget, QSlider, QGridLayout, QLabel) -from PyQt4.QtCore import Qt from util import ColorMixer From 449f3e4cbf19e4a89901eda5e6967318b75e5861 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 27 Jul 2012 22:58:20 -0400 Subject: [PATCH 135/648] STY: Refactor BaseWidget from Slider and ComboBox --- skimage/viewer/widgets/core.py | 39 ++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 6e48197e..55d0cfc3 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -16,12 +16,27 @@ from PyQt4 import QtGui from PyQt4 import QtCore -__all__ = ['Slider', 'ComboBox'] +__all__ = ['BaseWidget', 'Slider', 'ComboBox'] -#TODO: Add WidgetBase class (requires reimplementation of IntelligentSlider). +class BaseWidget(QtGui.QWidget): -class Slider(QtGui.QWidget): + def __init__(self, name, ptype, callback): + super(BaseWidget, self).__init__() + self.name = name + self.ptype = ptype + self.callback = callback + + @property + def val(self): + msg = "Subclass of BaseWidget requires `val` property" + raise NotImplementedError(msg) + + def _value_changed(self, value): + self.callback(self.name, value) + + +class Slider(BaseWidget): """Slider widget. 'name' attribute and calls a callback @@ -56,10 +71,7 @@ class Slider(QtGui.QWidget): def __init__(self, name, low=0.0, high=1.0, value=None, ptype='kwarg', callback=None, max_edit_width=60, orientation='horizontal', update_on='move'): - super(Slider, self).__init__() - self.name = name - self.ptype = ptype - self.callback = callback + super(Slider, self).__init__(name, ptype, callback) # divide slider into 1000 discrete values slider_min = 0 @@ -142,11 +154,8 @@ class Slider(QtGui.QWidget): def val(self): return self.slider.value() * self._scale + self._low - def _value_changed(self, value): - self.callback(self.name, value) - -class ComboBox(QtGui.QWidget): +class ComboBox(BaseWidget): """ComboBox widget for selecting among a list of choices. Parameters @@ -163,11 +172,8 @@ class ComboBox(QtGui.QWidget): """ def __init__(self, name, items, ptype='kwarg', callback=None): - super(ComboBox, self).__init__() - self.ptype = ptype - self.callback = callback + super(ComboBox, self).__init__(name, ptype, callback) - self.name = name self.name_label = QtGui.QLabel() self.name_label.setText(self.name) self.name_label.setAlignment(QtCore.Qt.AlignLeft) @@ -186,6 +192,3 @@ class ComboBox(QtGui.QWidget): @property def val(self): return self._combo_box.value() - - def _value_changed(self, value): - self.callback(self.name, value) From b6045a8d5f1adc00860ab55e40f4201dd0e27f63 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 28 Jul 2012 00:13:33 -0400 Subject: [PATCH 136/648] BUG: Fix behavior when initial overlay limits are bad. Intensity limits are calculated by the initial input image. If this image has, for example, all black pixels, then subsequent overlays will remain all black because of the initialized limits. Set limits based on data type to fix this issue. --- skimage/viewer/plugins/overlayplugin.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index dbf15e7d..11ca5ab5 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -1,7 +1,15 @@ +import numpy as np + +from skimage.util import dtype from .base import Plugin from ..utils import ClearColormap +#TODO: Maybe this bool definition should be moved to skimage.util.dtype. +dtype_range = dtype.dtype_range.copy() +dtype_range[np.bool_] = (False, True) + + class OverlayPlugin(Plugin): """Plugin for ImageViewer that displays an overlay on top of main image. @@ -47,7 +55,9 @@ class OverlayPlugin(Plugin): ax.images.remove(self._overlay_plot) self._overlay_plot = None elif self._overlay_plot is None: - self._overlay_plot = ax.imshow(image, cmap=self.cmap) + vmin, vmax = dtype_range[image.dtype.type] + self._overlay_plot = ax.imshow(image, cmap=self.cmap, + vmin=vmin, vmax=vmax) else: self._overlay_plot.set_array(image) self.image_viewer.redraw() From 1000c73ceb1dd38ac1e67320b471ab5fd4c9571f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 28 Jul 2012 11:40:04 -0400 Subject: [PATCH 137/648] DOC: Explain use of callback parameter. --- skimage/viewer/widgets/core.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 55d0cfc3..23bc46b7 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -62,7 +62,8 @@ class Slider(BaseWidget): ptype : {'arg' | 'kwarg' | 'plugin'} Parameter type. callback : function - Callback function called in response to slider changes. + Callback function called in response to slider changes. This function + is typically set when the widget is added to a plugin. orientation : {'horizontal' | 'vertical'} Slider orientation. update_on : {'move' | 'release'} @@ -169,6 +170,9 @@ class ComboBox(BaseWidget): Allowed parameter values. ptype : {'arg' | 'kwarg' | 'plugin'} Parameter type. + callback : function + Callback function called in response to slider changes. This function + is typically set when the widget is added to a plugin. """ def __init__(self, name, items, ptype='kwarg', callback=None): From 7d533e19a4505650476c4fbbec680818c2ad589e Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 28 Jul 2012 11:43:20 -0400 Subject: [PATCH 138/648] DOC: Add explanation of add operator. --- skimage/viewer/widgets/core.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 23bc46b7..826dc4f5 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -2,8 +2,12 @@ Widgets for interacting with ImageViewer. These widgets should be added to a Plugin subclass using its `add_widget` -method. The Plugin will delegate action based on the widget's parameter type -specified by its `ptype` attribute, which can be: +method or calling:: + + plugin += Widget(...) + +on a Plugin instance. The Plugin will delegate action based on the widget's +parameter type specified by its `ptype` attribute, which can be: 'arg' : positional argument passed to Plugin's `filter_image` method. 'kwarg' : keyword argument passed to Plugin's `filter_image` method. From 3340d0612dbc7dc8dfd8273346e01be467fbecff Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 28 Jul 2012 14:38:46 -0400 Subject: [PATCH 139/648] BUG: Fix scaling when setting default slider value. --- skimage/viewer/widgets/core.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 826dc4f5..31d177ef 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -81,9 +81,11 @@ class Slider(BaseWidget): # divide slider into 1000 discrete values slider_min = 0 slider_max = 1000 - if value is None: - value = 500 scale = float(high - low) / slider_max + if value is None: + slider_value = 500 + else: + slider_value = (value - low) / scale self._scale = scale self._low = low self._high = high @@ -106,7 +108,7 @@ class Slider(BaseWidget): self.slider = QtGui.QSlider(orientation_slider) self.slider.setRange(slider_min, slider_max) - self.slider.setValue(value) + self.slider.setValue(slider_value) if update_on == 'move': self.slider.valueChanged.connect(self._on_slider_changed) elif update_on == 'release': From b54817c5b39e46ea71bd905d04912dda0b2f640c Mon Sep 17 00:00:00 2001 From: Dharhas Pothina Date: Mon, 30 Jul 2012 10:07:33 -0500 Subject: [PATCH 140/648] minor cleanup of comments & move ref_white to global lab_ref_white --- skimage/color/colorconv.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index ebc18bfb..75b7649b 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -288,6 +288,9 @@ grey_from_rgb = np.array([[0.2125, 0.7154, 0.0721], [0, 0, 0], [0, 0, 0]]) +# CIE LAB constants for Observer= 2A, Illuminant= D65 +lab_ref_white = np.array([0.95047, 1., 1.08883]) + #------------------------------------------------------------- # The conversion functions that make use of the matrices above #------------------------------------------------------------- @@ -547,10 +550,6 @@ def gray2rgb(image): return np.dstack((image, image, image)) -#-------------------------------------------------------------- -# The conversion functions that make use of the constants above -#-------------------------------------------------------------- - def xyz2lab(xyz): """XYZ to CIE-LAB color space conversion. @@ -581,24 +580,16 @@ def xyz2lab(xyz): Examples -------- - >>> import os - >>> from skimage import data_dir + >>> from skimage import data >>> from skimage.color import rgb2xyz, xyz2lab - >>> from skimage.io import imread - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena = data.lena() >>> lena_xyz = rgb2xyz(lena) >>> lena_lab = xyz2lab(lena_xyz) """ arr = _prepare_colorarray(xyz) - #---------------------- - # Constants for CIE LAB - #---------------------- - # Observer= 2A, Illuminant= D65 - ref_white = np.array([0.95047, 1., 1.08883]) - # scale by CIE XYZ tristimulus values of the reference white point - arr = arr / ref_white + arr = arr / lab_ref_white # Nonlinear distortion and linear transformation mask = arr > 0.008856 @@ -659,8 +650,7 @@ def lab2xyz(lab): out[~mask] = (out[~mask] - 16.0 / 116.) / 7.787 # rescale Observer= 2 deg, Illuminant= D65 - ref_white = np.array([0.95047, 1., 1.08883]) - out *= ref_white + out *= lab_ref_white return out From f74ca8685b165ab99dd7a70fa40d4c6e2febd4be Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Tue, 31 Jul 2012 22:00:32 +0100 Subject: [PATCH 141/648] MISC minor cleanup in doctests / examples --- skimage/color/colorconv.py | 61 ++++++++++++-------------------------- 1 file changed, 19 insertions(+), 42 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 75b7649b..2640f3be 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -83,11 +83,8 @@ def convert_colorspace(arr, fromspace, tospace): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> from skimage import data + >>> lena = data.lena() >>> lena_hsv = convert_colorspace(lena, 'RGB', 'HSV') """ fromdict = {'RGB': lambda im: im, 'HSV': hsv2rgb, 'RGB CIE': rgbcie2rgb, @@ -151,11 +148,9 @@ def rgb2hsv(rgb): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> from skimage import color + >>> from skimage import data + >>> lena = data.lena() >>> lena_hsv = color.rgb2hsv(lena) """ arr = _prepare_colorarray(rgb) @@ -226,11 +221,8 @@ def hsv2rgb(hsv): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> from skimage import data + >>> lena = data.lena() >>> lena_hsv = rgb2hsv(lena) >>> lena_rgb = hsv2rgb(lena_hsv) """ @@ -351,14 +343,11 @@ def xyz2rgb(xyz): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread + >>> from skimage import data >>> from skimage.color import rgb2xyz, xyz2rgb - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena = data.lena() >>> lena_xyz = rgb2xyz(lena) - >>> lena_rgb = xyz2rgb(lena_hsv) + >>> lena_rgb = xyz2rgb(lena_xyz) """ return _convert(rgb_from_xyz, xyz) @@ -392,11 +381,8 @@ def rgb2xyz(rgb): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> from skimage import data + >>> lena = data.lena() >>> lena_xyz = rgb2xyz(lena) """ return _convert(xyz_from_rgb, rgb) @@ -426,12 +412,9 @@ def rgb2rgbcie(rgb): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread + >>> from skimage import data >>> from skimage.color import rgb2rgbcie - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena = data.lena() >>> lena_rgbcie = rgb2rgbcie(lena) """ return _convert(rgbcie_from_rgb, rgb) @@ -461,14 +444,11 @@ def rgbcie2rgb(rgbcie): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread + >>> from skimage import data >>> from skimage.color import rgb2rgbcie, rgbcie2rgb - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena = data.lena() >>> lena_rgbcie = rgb2rgbcie(lena) - >>> lena_rgb = rgbcie2rgb(lena_hsv) + >>> lena_rgb = rgbcie2rgb(lena_rgbcie) """ return _convert(rgb_from_rgbcie, rgbcie) @@ -508,12 +488,9 @@ def rgb2grey(rgb): Examples -------- - >>> import os - >>> from skimage import data_dir - >>> from skimage.io import imread >>> from skimage.color import rgb2grey - - >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> from skimage import data + >>> lena = data.lena() >>> lena_grey = rgb2grey(lena) """ if rgb.ndim == 2: From ce017cc035e22ea05fa635f48b378c1afe79d111 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 1 Aug 2012 00:11:48 -0400 Subject: [PATCH 142/648] ENH: Add Slider `value_type` to allow int values. --- skimage/viewer/widgets/core.py | 67 +++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 31d177ef..7e1b6e9e 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -63,6 +63,8 @@ class Slider(BaseWidget): Range of slider values. value : float Default slider value. If None, use midpoint between `low` and `high`. + value : {'float' | 'int'} + Numeric type of slider value. ptype : {'arg' | 'kwarg' | 'plugin'} Parameter type. callback : function @@ -73,31 +75,24 @@ class Slider(BaseWidget): update_on : {'move' | 'release'} Control when callback function is called: on slider move or release. """ - def __init__(self, name, low=0.0, high=1.0, value=None, ptype='kwarg', - callback=None, max_edit_width=60, orientation='horizontal', - update_on='move'): + def __init__(self, name, low=0.0, high=1.0, value=None, value_type='float', + ptype='kwarg', callback=None, max_edit_width=60, + orientation='horizontal', update_on='move'): super(Slider, self).__init__(name, ptype, callback) - # divide slider into 1000 discrete values - slider_min = 0 - slider_max = 1000 - scale = float(high - low) / slider_max if value is None: - slider_value = 500 - else: - slider_value = (value - low) / scale - self._scale = scale - self._low = low - self._high = high + value = (high - low) / 2. + # Set widget orientation + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if orientation == 'vertical': - orientation_slider = Qt.Vertical + self.slider = QtGui.QSlider(Qt.Vertical) alignment = QtCore.Qt.AlignHCenter align_text = QtCore.Qt.AlignHCenter align_value = QtCore.Qt.AlignHCenter self.layout = QtGui.QVBoxLayout(self) elif orientation == 'horizontal': - orientation_slider = Qt.Horizontal + self.slider = QtGui.QSlider(Qt.Horizontal) alignment = QtCore.Qt.AlignVCenter align_text = QtCore.Qt.AlignLeft align_value = QtCore.Qt.AlignRight @@ -105,10 +100,30 @@ class Slider(BaseWidget): else: msg = "Unexpected value %s for 'orientation'" raise ValueError(msg % orientation) + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + # Set slider behavior for float and int values. + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + if value_type == 'float': + # divide slider into 1000 discrete values + slider_max = 1000 + self._scale = float(high - low) / slider_max + self.slider.setRange(0, slider_max) + self.value_fmt = '%2.2f' + elif value_type == 'int': + self.slider.setRange(low, high) + self.value_fmt = '%d' + else: + msg = "Expected `value_type` to be 'float' or 'int'; received: %s" + raise ValueError(msg % value_type) + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + self.value_type = value_type + self._low = low + self._high = high + # Update slider position to default value + self.val = value - self.slider = QtGui.QSlider(orientation_slider) - self.slider.setRange(slider_min, slider_max) - self.slider.setValue(slider_value) if update_on == 'move': self.slider.valueChanged.connect(self._on_slider_changed) elif update_on == 'release': @@ -122,7 +137,7 @@ class Slider(BaseWidget): self.editbox = QtGui.QLineEdit() self.editbox.setMaximumWidth(max_edit_width) - self.editbox.setText('%2.2f' % self.val) + self.editbox.setText(self.value_fmt % self.val) self.editbox.setAlignment(align_value) self.editbox.editingFinished.connect(self._on_editbox_changed) @@ -147,8 +162,7 @@ class Slider(BaseWidget): self._bad_editbox_input() return - slider_value = (value - self._low) / self._scale - self.slider.setValue(slider_value) + self.val = value self._good_editbox_input() def _good_editbox_input(self): @@ -159,7 +173,16 @@ class Slider(BaseWidget): @property def val(self): - return self.slider.value() * self._scale + self._low + value = self.slider.value() + if self.value_type == 'float': + value = value * self._scale + self._low + return value + + @val.setter + def val(self, value): + if self.value_type == 'float': + value = (value - self._low) / self._scale + self.slider.setValue(value) class ComboBox(BaseWidget): From f614afaa08052d890597df96184d555041aca9e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 2 Aug 2012 07:46:34 +0200 Subject: [PATCH 143/648] fix orientation of regionprops with correct quadrant determination --- skimage/measure/_regionprops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 396c6bdd..ee58868c 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -1,5 +1,5 @@ # coding: utf-8 -from math import sqrt, atan, pi as PI +from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage @@ -301,7 +301,7 @@ def regionprops(label_image, properties=['Area', 'Centroid'], if a - c == 0: obj_props['Orientation'] = PI / 2 else: - obj_props['Orientation'] = - 0.5 * atan(2 * b / (a - c)) + obj_props['Orientation'] = - 0.5 * atan2(2 * b, (a - c)) if 'Perimeter' in properties: obj_props['Perimeter'] = perimeter(array, 4) From 13caa41fa73174f882c33c70144a87a431201c9f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 2 Aug 2012 22:49:53 -0400 Subject: [PATCH 144/648] BUG: Rename modules with duplicate function names. Modules with functions of the same name can cause confusion (in general) and causes issues when running `nosetests --with-doctest`. --- skimage/filter/__init__.py | 6 +++--- skimage/filter/{canny.py => _canny.py} | 0 skimage/filter/{rank_order.py => _rank_order.py} | 0 skimage/filter/{tv_denoise.py => _tv_denoise.py} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename skimage/filter/{canny.py => _canny.py} (100%) rename skimage/filter/{rank_order.py => _rank_order.py} (100%) rename skimage/filter/{tv_denoise.py => _tv_denoise.py} (100%) diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index 04c972cc..bdbbb531 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,7 +1,7 @@ from .lpi_filter import * from .ctmf import median_filter -from .canny import canny +from ._canny import canny from .edges import sobel, hsobel, vsobel, hprewitt, vprewitt, prewitt -from .tv_denoise import tv_denoise -from .rank_order import rank_order +from ._tv_denoise import tv_denoise +from ._rank_order import rank_order from .thresholding import threshold_otsu, threshold_adaptive diff --git a/skimage/filter/canny.py b/skimage/filter/_canny.py similarity index 100% rename from skimage/filter/canny.py rename to skimage/filter/_canny.py diff --git a/skimage/filter/rank_order.py b/skimage/filter/_rank_order.py similarity index 100% rename from skimage/filter/rank_order.py rename to skimage/filter/_rank_order.py diff --git a/skimage/filter/tv_denoise.py b/skimage/filter/_tv_denoise.py similarity index 100% rename from skimage/filter/tv_denoise.py rename to skimage/filter/_tv_denoise.py From 8a340cc47dbe7674b10810a9d12661c58b67083d Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 2 Aug 2012 23:08:19 -0400 Subject: [PATCH 145/648] BUG: more module renaming to prevent duplicates. --- skimage/feature/__init__.py | 4 ++-- skimage/feature/{harris.py => _harris.py} | 0 skimage/feature/{hog.py => _hog.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename skimage/feature/{harris.py => _harris.py} (100%) rename skimage/feature/{hog.py => _hog.py} (100%) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 70c154b8..c067268a 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -1,5 +1,5 @@ -from .hog import hog +from ._hog import hog from .greycomatrix import greycomatrix, greycoprops from .peak import peak_local_max -from .harris import harris +from ._harris import harris from .template import match_template diff --git a/skimage/feature/harris.py b/skimage/feature/_harris.py similarity index 100% rename from skimage/feature/harris.py rename to skimage/feature/_harris.py diff --git a/skimage/feature/hog.py b/skimage/feature/_hog.py similarity index 100% rename from skimage/feature/hog.py rename to skimage/feature/_hog.py From b8c0663332398d0487c3e43d205cb02b478b243d Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 15 Jun 2012 20:59:17 +0200 Subject: [PATCH 146/648] Add segmentation setup.py for felsenzwalb algorithm --- skimage/segmentation/felsenzwalb.pyx | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 skimage/segmentation/felsenzwalb.pyx diff --git a/skimage/segmentation/felsenzwalb.pyx b/skimage/segmentation/felsenzwalb.pyx new file mode 100644 index 00000000..38a95411 --- /dev/null +++ b/skimage/segmentation/felsenzwalb.pyx @@ -0,0 +1,4 @@ +# Implements Felsenzwalb's efficient graph based image segmentation. +# Author: Andreas Mueller + +def felsenzwalb(np.ndarray[dtype= From 967eb5b50d959543bc41904fbdf651418acbac17 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 16 Jun 2012 16:42:18 +0200 Subject: [PATCH 147/648] ENH first draft of felzenszwalbs graph based image segmentation in Python --- .../plot_felzenszwalb_segmentation.py | 13 ++++ skimage/segmentation/__init__.py | 1 + skimage/segmentation/felzenszwalb.py | 60 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 doc/examples/plot_felzenszwalb_segmentation.py create mode 100644 skimage/segmentation/felzenszwalb.py diff --git a/doc/examples/plot_felzenszwalb_segmentation.py b/doc/examples/plot_felzenszwalb_segmentation.py new file mode 100644 index 00000000..dbcb2abb --- /dev/null +++ b/doc/examples/plot_felzenszwalb_segmentation.py @@ -0,0 +1,13 @@ +import matplotlib.pyplot as plt +import numpy as np + +from skimage.data import lena +from skimage.segmentation import felzenszwalb_segmentation + +img = lena() +segments = felzenszwalb_segmentation(img, k=1000) +plt.imshow(img) +plt.figure() +plt.imshow(segments) +plt.show() +print("num segments: %d" % len(np.unique(segments))) diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 118c3ec2..372c58fc 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1 +1,2 @@ from .random_walker_segmentation import random_walker +from .felzenszwalb import felzenszwalb_segmentation diff --git a/skimage/segmentation/felzenszwalb.py b/skimage/segmentation/felzenszwalb.py new file mode 100644 index 00000000..9dea47a6 --- /dev/null +++ b/skimage/segmentation/felzenszwalb.py @@ -0,0 +1,60 @@ +import numpy as np +from collections import defaultdict +import scipy + +#from ..util import img_as_float +#from ..color import rgb2grey +from .union_find import UnionFind + +from IPython.core.debugger import Tracer +tracer = Tracer() + + +def felzenszwalb_segmentation(image, k, sigma=0.8): + k = float(k) + #image = img_as_float(image) + #image = rgb2grey(image) + image = image[:, :, 0] + image = scipy.ndimage.gaussian_filter(image, sigma=sigma) + + # compute edge weights in 8 connectivity: + #right_cost = np.sum((image[1:, :, :] - image[:-1, :, :]) ** 2, axis=2) + #down_cost = np.sum((image[:, 1:, :] - image[:, :-1, :]) ** 2, axis=2) + right_cost = np.abs((image[1:, :] - image[:-1, :])) + down_cost = np.abs((image[:, 1:] - image[:, :-1])) + dright_cost = np.abs((image[1:, 1:] - image[:-1, :-1])) + uright_cost = np.abs((image[1:, :-1] - image[:-1, 1:])) + costs = np.hstack([right_cost.ravel(), down_cost.ravel(), + dright_cost.ravel(), uright_cost.ravel()]) + # compute edges between pixels: + width, height = image.shape[:2] + indices = np.arange(width * height).reshape(width, height) + right_edges = np.c_[indices[1:, :].ravel(), indices[:-1, :].ravel()] + down_edges = np.c_[indices[:, 1:].ravel(), indices[:, :-1].ravel()] + dright_edges = np.c_[indices[1:, 1:].ravel(), indices[:-1, :-1].ravel()] + uright_edges = np.c_[indices[:-1, 1:].ravel(), indices[1:, :-1].ravel()] + edges = np.vstack([right_edges, down_edges, dright_edges, uright_edges]) + # initialize data structures for segment size + # and inner cost, then start greedy iteration over edges. + edge_queue = np.argsort(costs) + segments = UnionFind() + segment_size = defaultdict(lambda: 1) + # inner cost of segments + cint = defaultdict(lambda: 0) + for edge, cost in zip(edges[edge_queue], costs[edge_queue]): + seg0 = segments[edge[0]] + seg1 = segments[edge[1]] + if seg0 == seg1: + continue + inner_cost0 = cint[seg0] + k / segment_size[seg0] + inner_cost1 = cint[seg1] + k / segment_size[seg1] + if cost < min(inner_cost0, inner_cost1): + seg_new = segments.union(seg0, seg1) + # update size and cost + segment_size[seg_new] = segment_size[seg0] + segment_size[seg1] + cint[seg_new] = cost + out = np.zeros(width * height, dtype=np.int) + for i in xrange(width * height): + out[i] = segments[i] + out = out.reshape(width, height) + return out From e2d60f01357d9d18ee3c34be4b7953e7dd634168 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 16 Jun 2012 17:28:43 +0200 Subject: [PATCH 148/648] ENH using union find from morphology module --- skimage/morphology/ccomp.pxd | 10 +++++++ skimage/segmentation/felsenzwalb.pyx | 4 --- .../{felzenszwalb.py => felzenszwalb.pyx} | 20 +++++++------- skimage/segmentation/setup.py | 27 +++++++++++++++++++ skimage/setup.py | 1 + 5 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 skimage/morphology/ccomp.pxd delete mode 100644 skimage/segmentation/felsenzwalb.pyx rename skimage/segmentation/{felzenszwalb.py => felzenszwalb.pyx} (82%) create mode 100644 skimage/segmentation/setup.py diff --git a/skimage/morphology/ccomp.pxd b/skimage/morphology/ccomp.pxd new file mode 100644 index 00000000..0b431832 --- /dev/null +++ b/skimage/morphology/ccomp.pxd @@ -0,0 +1,10 @@ +"""Export fast union find in Cython""" +cimport numpy as np + +DTYPE = np.int +ctypedef np.int_t DTYPE_t + +cdef DTYPE_t find_root(np.int_t *forest, np.int_t n) +cdef set_root(np.int_t *forest, np.int_t n, np.int_t root) +cdef join_trees(np.int_t *forest, np.int_t n, np.int_t m) +cdef link_bg(np.int_t *forest, np.int_t n, np.int_t *background_node) diff --git a/skimage/segmentation/felsenzwalb.pyx b/skimage/segmentation/felsenzwalb.pyx deleted file mode 100644 index 38a95411..00000000 --- a/skimage/segmentation/felsenzwalb.pyx +++ /dev/null @@ -1,4 +0,0 @@ -# Implements Felsenzwalb's efficient graph based image segmentation. -# Author: Andreas Mueller - -def felsenzwalb(np.ndarray[dtype= diff --git a/skimage/segmentation/felzenszwalb.py b/skimage/segmentation/felzenszwalb.pyx similarity index 82% rename from skimage/segmentation/felzenszwalb.py rename to skimage/segmentation/felzenszwalb.pyx index 9dea47a6..5a0ecbac 100644 --- a/skimage/segmentation/felzenszwalb.py +++ b/skimage/segmentation/felzenszwalb.pyx @@ -1,10 +1,11 @@ import numpy as np +cimport numpy as np from collections import defaultdict import scipy #from ..util import img_as_float #from ..color import rgb2grey -from .union_find import UnionFind +from skimage.morphology.ccomp cimport find_root, join_trees from IPython.core.debugger import Tracer tracer = Tracer() @@ -37,24 +38,23 @@ def felzenszwalb_segmentation(image, k, sigma=0.8): # initialize data structures for segment size # and inner cost, then start greedy iteration over edges. edge_queue = np.argsort(costs) - segments = UnionFind() + cdef np.ndarray[np.int_t, ndim=2] segments = indices.reshape(width, height) + cdef np.int_t *segments_p = segments.data + cdef np.int_t seg_new segment_size = defaultdict(lambda: 1) # inner cost of segments cint = defaultdict(lambda: 0) for edge, cost in zip(edges[edge_queue], costs[edge_queue]): - seg0 = segments[edge[0]] - seg1 = segments[edge[1]] + seg0 = find_root(segments_p, edge[0]) + seg1 = find_root(segments_p, edge[1]) if seg0 == seg1: continue inner_cost0 = cint[seg0] + k / segment_size[seg0] inner_cost1 = cint[seg1] + k / segment_size[seg1] if cost < min(inner_cost0, inner_cost1): - seg_new = segments.union(seg0, seg1) # update size and cost + join_trees(segments_p, seg0, seg1) + seg_new = find_root(segments_p, seg0) segment_size[seg_new] = segment_size[seg0] + segment_size[seg1] cint[seg_new] = cost - out = np.zeros(width * height, dtype=np.int) - for i in xrange(width * height): - out[i] = segments[i] - out = out.reshape(width, height) - return out + return segments diff --git a/skimage/segmentation/setup.py b/skimage/segmentation/setup.py new file mode 100644 index 00000000..8f6361bf --- /dev/null +++ b/skimage/segmentation/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('segmentation', parent_package, top_path) + + cython(['felzenszwalb.pyx'], working_path=base_path) + config.add_extension('felzenszwalb', sources=['felzenszwalb.c'], + include_dirs=[get_numpy_include_dirs()]) + + return config + +if __name__ == '__main__': + from numpy.distutils.core import setup + setup(maintainer = 'scikits-image Developers', + maintainer_email = 'scikits-image@googlegroups.com', + description = 'Segmentation Algorithms', + url = 'https://github.com/scikits-image/scikits-image', + license = 'SciPy License (BSD Style)', + **(configuration(top_path='').todict()) + ) diff --git a/skimage/setup.py b/skimage/setup.py index 0afc5bc9..02c0b52d 100644 --- a/skimage/setup.py +++ b/skimage/setup.py @@ -17,6 +17,7 @@ def configuration(parent_package='', top_path=None): config.add_subpackage('morphology') config.add_subpackage('transform') config.add_subpackage('util') + config.add_subpackage('segmentation') def add_test_directories(arg, dirname, fnames): if dirname.split(os.path.sep)[-1] == 'tests': From b1b1c343b452b32371db7a5c6422afd8fce16c4c Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 16 Jun 2012 19:15:20 +0200 Subject: [PATCH 149/648] MISC remove debugging tracer, unnecessary variable. --- skimage/segmentation/felzenszwalb.pyx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/skimage/segmentation/felzenszwalb.pyx b/skimage/segmentation/felzenszwalb.pyx index 5a0ecbac..a53bf70e 100644 --- a/skimage/segmentation/felzenszwalb.pyx +++ b/skimage/segmentation/felzenszwalb.pyx @@ -7,9 +7,6 @@ import scipy #from ..color import rgb2grey from skimage.morphology.ccomp cimport find_root, join_trees -from IPython.core.debugger import Tracer -tracer = Tracer() - def felzenszwalb_segmentation(image, k, sigma=0.8): k = float(k) @@ -29,16 +26,15 @@ def felzenszwalb_segmentation(image, k, sigma=0.8): dright_cost.ravel(), uright_cost.ravel()]) # compute edges between pixels: width, height = image.shape[:2] - indices = np.arange(width * height).reshape(width, height) - right_edges = np.c_[indices[1:, :].ravel(), indices[:-1, :].ravel()] - down_edges = np.c_[indices[:, 1:].ravel(), indices[:, :-1].ravel()] - dright_edges = np.c_[indices[1:, 1:].ravel(), indices[:-1, :-1].ravel()] - uright_edges = np.c_[indices[:-1, 1:].ravel(), indices[1:, :-1].ravel()] + cdef np.ndarray[np.int_t, ndim=2] segments = np.arange(width * height).reshape(width, height) + right_edges = np.c_[segments[1:, :].ravel(), segments[:-1, :].ravel()] + down_edges = np.c_[segments[:, 1:].ravel(), segments[:, :-1].ravel()] + dright_edges = np.c_[segments[1:, 1:].ravel(), segments[:-1, :-1].ravel()] + uright_edges = np.c_[segments[:-1, 1:].ravel(), segments[1:, :-1].ravel()] edges = np.vstack([right_edges, down_edges, dright_edges, uright_edges]) # initialize data structures for segment size # and inner cost, then start greedy iteration over edges. edge_queue = np.argsort(costs) - cdef np.ndarray[np.int_t, ndim=2] segments = indices.reshape(width, height) cdef np.int_t *segments_p = segments.data cdef np.int_t seg_new segment_size = defaultdict(lambda: 1) From 40ecdd29dbce103f563de7a62f6628a45a3d61ac Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 16 Jun 2012 19:15:43 +0200 Subject: [PATCH 150/648] ENH naive pure python implementation of quickshift --- skimage/segmentation/quickshift.py | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 skimage/segmentation/quickshift.py diff --git a/skimage/segmentation/quickshift.py b/skimage/segmentation/quickshift.py new file mode 100644 index 00000000..5025b3a2 --- /dev/null +++ b/skimage/segmentation/quickshift.py @@ -0,0 +1,47 @@ +import numpy as np +from itertools import product, combinations_with_replacement + +from IPython.core.debugger import Tracer +tracer = Tracer() + + +def quickshift(image, sigma=5, tau=10): + # do smoothing beforehand? + width, height = image.shape[:2] + densities = np.zeros((width, height)) + w = 10 + + # TODO: normalize density by number of considered points. + # important for the border! + # compute densities + for x, y in product(xrange(width), xrange(height)): + current_pixel = np.hstack([image[x, y, :], x, y]) + for xx, yy in combinations_with_replacement(xrange(-w / 2, w / 2), 2): + x_, y_ = x + xx, y + yy + if 0 <= x_ < width and 0 <= y_ < height: + other_pixel = np.hstack([image[x_, y_, :], x_, y_]) + dist = np.sum((current_pixel - other_pixel) ** 2) + densities[x, y] += np.exp(-dist / sigma) + + # default parent to self: + parent = np.arange(width * height).reshape(width, height) + # find nearest node with higher density + for x, y in product(xrange(width), xrange(height)): + current_density = densities[x, y] + current_pixel = np.hstack([image[x, y, :], x, y]) + closest = np.inf + for xx, yy in combinations_with_replacement(xrange(-w / 2, w / 2), 2): + x_, y_ = x + xx, y + yy + if 0 <= x_ < width and 0 <= y_ < height: + if densities[x_, y_] > current_density: + other_pixel = np.hstack([image[x_, y_, :], x_, y_]) + dist = np.sum((current_pixel - other_pixel) ** 2) + if dist < closest: + closest = dist + parent[x, y] = x_ * width + y_ + flat = parent.ravel() + old = np.zeros_like(flat) + while (old != flat).any(): + old = flat + flat = flat[flat] + return flat.reshape(parent.shape) From eb5c2fe5d49ccf93c9499c000c17e6a8fbb45bbf Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 16 Jun 2012 20:52:04 +0200 Subject: [PATCH 151/648] ENH fixed stupid bug in quickshift, example --- doc/examples/plot_quickshift.py | 47 ++++++++++++++++++++++++++++ skimage/segmentation/__init__.py | 5 ++- skimage/segmentation/quickshift.py | 50 ++++++++++++++++++++++++------ 3 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 doc/examples/plot_quickshift.py diff --git a/doc/examples/plot_quickshift.py b/doc/examples/plot_quickshift.py new file mode 100644 index 00000000..80f216fa --- /dev/null +++ b/doc/examples/plot_quickshift.py @@ -0,0 +1,47 @@ +import matplotlib.pyplot as plt +import numpy as np + +from scipy import ndimage +#from skimage.data import lena +#from skimage.util import img_as_float +from skimage.segmentation import quickshift + +from IPython.core.debugger import Tracer +tracer = Tracer() + + +def microstructure(l=256): + """ + Synthetic binary data: binary microstructure with blobs. + + Parameters + ---------- + + l: int, optional + linear size of the returned image + """ + n = 5 + x, y = np.ogrid[0:l, 0:l] + mask = np.zeros((l, l)) + generator = np.random.RandomState(1) + points = l * generator.rand(2, n ** 2) + mask[(points[0]).astype(np.int), (points[1]).astype(np.int)] = 1 + mask = ndimage.gaussian_filter(mask, sigma=l / (4. * n)) + return (mask > mask.mean()).astype(np.float) + + +#img = img_as_float(lena()[250:300, 250:300]) +img = microstructure(l=50) +segments = quickshift(img.reshape(50, 50, 1)) +segments = np.unique(segments, return_inverse=True)[1].reshape(50, 50) +intensities = np.bincount(segments.ravel(), img.ravel()) +counts = np.bincount(segments.ravel()) +intensities /= counts + +plt.imshow(img, interpolation='nearest') +plt.figure() +plt.imshow(segments, interpolation='nearest') +plt.figure() +plt.imshow(intensities[segments], interpolation='nearest') +plt.show() +print("num segments: %d" % len(np.unique(segments))) diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 372c58fc..0ea91444 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1,2 +1,5 @@ from .random_walker_segmentation import random_walker -from .felzenszwalb import felzenszwalb_segmentation +#from .felzenszwalb import felzenszwalb_segmentation +from .quickshift import quickshift + +__all__ = [random_walker, quickshift] diff --git a/skimage/segmentation/quickshift.py b/skimage/segmentation/quickshift.py index 5025b3a2..65c8847f 100644 --- a/skimage/segmentation/quickshift.py +++ b/skimage/segmentation/quickshift.py @@ -1,36 +1,62 @@ import numpy as np -from itertools import product, combinations_with_replacement - -from IPython.core.debugger import Tracer -tracer = Tracer() +from itertools import product def quickshift(image, sigma=5, tau=10): - # do smoothing beforehand? + """Computes quickshift clustering in RGB-(x,y) space. + + Parameters + ---------- + image: ndarray, [width, height, channels] + Input image + sigma: float + Width of Gaussian kernel used in smoothing the + sample density. Higher means less clusters. + tau: float + Cut-off point for data distances. + Higher means less clusters. + + Returns + ------- + segment_mask: ndarray, [width, height] + Integer mask indicating segment labels. + """ + + # We compute the distances twice since otherwise + # we might get crazy memory overhead (width * height * windowsize**2) + + # TODO do smoothing beforehand? + # TODO manage borders somehow? + + # window size for neighboring pixels to consider + if sigma < 1: + raise ValueError("Sigma should be >= 1") + w = int(2 * sigma) + width, height = image.shape[:2] densities = np.zeros((width, height)) - w = 10 - # TODO: normalize density by number of considered points. - # important for the border! # compute densities for x, y in product(xrange(width), xrange(height)): current_pixel = np.hstack([image[x, y, :], x, y]) - for xx, yy in combinations_with_replacement(xrange(-w / 2, w / 2), 2): + for xx, yy in product(xrange(-w / 2, w / 2 + 1), repeat=2): x_, y_ = x + xx, y + yy if 0 <= x_ < width and 0 <= y_ < height: other_pixel = np.hstack([image[x_, y_, :], x_, y_]) dist = np.sum((current_pixel - other_pixel) ** 2) densities[x, y] += np.exp(-dist / sigma) + # this will break ties that otherwise would give us headache + densities += np.random.normal(scale=0.00001, size=densities.shape) # default parent to self: parent = np.arange(width * height).reshape(width, height) + dist_parent = np.zeros((width, height)) # find nearest node with higher density for x, y in product(xrange(width), xrange(height)): current_density = densities[x, y] current_pixel = np.hstack([image[x, y, :], x, y]) closest = np.inf - for xx, yy in combinations_with_replacement(xrange(-w / 2, w / 2), 2): + for xx, yy in product(xrange(-w / 2, w / 2 + 1), repeat=2): x_, y_ = x + xx, y + yy if 0 <= x_ < width and 0 <= y_ < height: if densities[x_, y_] > current_density: @@ -39,7 +65,11 @@ def quickshift(image, sigma=5, tau=10): if dist < closest: closest = dist parent[x, y] = x_ * width + y_ + dist_parent[x, y] = closest + + dist_parent = dist_parent.ravel() flat = parent.ravel() + flat[dist_parent > tau] = np.arange(width * height)[dist_parent > tau] old = np.zeros_like(flat) while (old != flat).any(): old = flat From 8c735b64708c0d73356d096e51a536c888124596 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 16 Jun 2012 21:09:07 +0200 Subject: [PATCH 152/648] ENH start cythonizing quickshift, get rid of hstack. --- .../{quickshift.py => quickshift.pyx} | 25 +++++++++++-------- skimage/segmentation/setup.py | 18 +++++++------ 2 files changed, 25 insertions(+), 18 deletions(-) rename skimage/segmentation/{quickshift.py => quickshift.pyx} (77%) diff --git a/skimage/segmentation/quickshift.py b/skimage/segmentation/quickshift.pyx similarity index 77% rename from skimage/segmentation/quickshift.py rename to skimage/segmentation/quickshift.pyx index 65c8847f..8bec6d2b 100644 --- a/skimage/segmentation/quickshift.py +++ b/skimage/segmentation/quickshift.pyx @@ -1,8 +1,10 @@ import numpy as np +cimport numpy as np + from itertools import product -def quickshift(image, sigma=5, tau=10): +def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, tau=10): """Computes quickshift clustering in RGB-(x,y) space. Parameters @@ -31,37 +33,38 @@ def quickshift(image, sigma=5, tau=10): # window size for neighboring pixels to consider if sigma < 1: raise ValueError("Sigma should be >= 1") - w = int(2 * sigma) + cdef int w = int(2 * sigma) + + cdef int width = image.shape[0] + cdef int height = image.shape[1] - width, height = image.shape[:2] - densities = np.zeros((width, height)) + cdef np.ndarray[dtype=np.float_t, ndim=2] densities = np.zeros((width, height)) # compute densities for x, y in product(xrange(width), xrange(height)): - current_pixel = np.hstack([image[x, y, :], x, y]) + current_pixel = image[x, y, :] for xx, yy in product(xrange(-w / 2, w / 2 + 1), repeat=2): x_, y_ = x + xx, y + yy if 0 <= x_ < width and 0 <= y_ < height: - other_pixel = np.hstack([image[x_, y_, :], x_, y_]) - dist = np.sum((current_pixel - other_pixel) ** 2) + dist = np.sum((current_pixel - image[x_, y_, :])**2) + (x - x_)**2 + (y - y_)**2 densities[x, y] += np.exp(-dist / sigma) # this will break ties that otherwise would give us headache - densities += np.random.normal(scale=0.00001, size=densities.shape) + + densities += np.random.normal(scale=0.00001, size=(width, height)) # default parent to self: parent = np.arange(width * height).reshape(width, height) dist_parent = np.zeros((width, height)) # find nearest node with higher density for x, y in product(xrange(width), xrange(height)): current_density = densities[x, y] - current_pixel = np.hstack([image[x, y, :], x, y]) + current_pixel = image[x, y, :] closest = np.inf for xx, yy in product(xrange(-w / 2, w / 2 + 1), repeat=2): x_, y_ = x + xx, y + yy if 0 <= x_ < width and 0 <= y_ < height: if densities[x_, y_] > current_density: - other_pixel = np.hstack([image[x_, y_, :], x_, y_]) - dist = np.sum((current_pixel - other_pixel) ** 2) + dist = np.sum((current_pixel - image[x_, y_, :])**2) + (x - x_)**2 + (y - y_)**2 if dist < closest: closest = dist parent[x, y] = x_ * width + y_ diff --git a/skimage/segmentation/setup.py b/skimage/segmentation/setup.py index 8f6361bf..b6f8d1e2 100644 --- a/skimage/segmentation/setup.py +++ b/skimage/segmentation/setup.py @@ -5,23 +5,27 @@ 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('segmentation', parent_package, top_path) - cython(['felzenszwalb.pyx'], working_path=base_path) - config.add_extension('felzenszwalb', sources=['felzenszwalb.c'], + #cython(['felzenszwalb.pyx'], working_path=base_path) + #config.add_extension('felzenszwalb', sources=['felzenszwalb.c'], + #include_dirs=[get_numpy_include_dirs()]) + cython(['quickshift.pyx'], working_path=base_path) + config.add_extension('quickshift', sources=['quickshift.c'], include_dirs=[get_numpy_include_dirs()]) return config if __name__ == '__main__': from numpy.distutils.core import setup - setup(maintainer = 'scikits-image Developers', - maintainer_email = 'scikits-image@googlegroups.com', - description = 'Segmentation Algorithms', - url = 'https://github.com/scikits-image/scikits-image', - license = 'SciPy License (BSD Style)', + setup(maintainer='scikits-image Developers', + maintainer_email='scikits-image@googlegroups.com', + description='Segmentation Algorithms', + url='https://github.com/scikits-image/scikits-image', + license='SciPy License (BSD Style)', **(configuration(top_path='').todict()) ) From de52692a9d46fcd875869dff1c032b2ce7e0ce15 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 16 Jun 2012 21:35:15 +0200 Subject: [PATCH 153/648] misc Uncomment Felzenszwalb as it is not messing with quickshift. --- skimage/segmentation/setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/segmentation/setup.py b/skimage/segmentation/setup.py index b6f8d1e2..a197555f 100644 --- a/skimage/segmentation/setup.py +++ b/skimage/segmentation/setup.py @@ -11,9 +11,9 @@ def configuration(parent_package='', top_path=None): config = Configuration('segmentation', parent_package, top_path) - #cython(['felzenszwalb.pyx'], working_path=base_path) - #config.add_extension('felzenszwalb', sources=['felzenszwalb.c'], - #include_dirs=[get_numpy_include_dirs()]) + cython(['felzenszwalb.pyx'], working_path=base_path) + config.add_extension('felzenszwalb', sources=['felzenszwalb.c'], + include_dirs=[get_numpy_include_dirs()]) cython(['quickshift.pyx'], working_path=base_path) config.add_extension('quickshift', sources=['quickshift.c'], include_dirs=[get_numpy_include_dirs()]) From 48fa3252beb34ce31601830d8ad742cb334d4c71 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 16 Jun 2012 21:53:28 +0200 Subject: [PATCH 154/648] ENH reasonable speed. --- skimage/segmentation/quickshift.pyx | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index 8bec6d2b..5fd3eafe 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -3,6 +3,9 @@ cimport numpy as np from itertools import product +cdef extern from "math.h": + double exp(double) + def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, tau=10): """Computes quickshift clustering in RGB-(x,y) space. @@ -37,6 +40,9 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta cdef int width = image.shape[0] cdef int height = image.shape[1] + cdef int channels = image.shape[2] + cdef float closest, dist + cdef int x, y, xx, yy, x_, y_ cdef np.ndarray[dtype=np.float_t, ndim=2] densities = np.zeros((width, height)) @@ -46,15 +52,18 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta for xx, yy in product(xrange(-w / 2, w / 2 + 1), repeat=2): x_, y_ = x + xx, y + yy if 0 <= x_ < width and 0 <= y_ < height: - dist = np.sum((current_pixel - image[x_, y_, :])**2) + (x - x_)**2 + (y - y_)**2 - densities[x, y] += np.exp(-dist / sigma) + dist = 0 + for c in xrange(channels): + dist += (current_pixel[c] - image[x_, y_, c])**2 + dist += (x - x_)**2 + (y - y_)**2 + densities[x, y] += float(exp(-dist / sigma)) # this will break ties that otherwise would give us headache densities += np.random.normal(scale=0.00001, size=(width, height)) # default parent to self: - parent = np.arange(width * height).reshape(width, height) - dist_parent = np.zeros((width, height)) + cdef np.ndarray[dtype=np.int_t, ndim=2] parent = np.arange(width * height).reshape(width, height) + cdef np.ndarray[dtype=np.float_t, ndim=2] dist_parent = np.zeros((width, height)) # find nearest node with higher density for x, y in product(xrange(width), xrange(height)): current_density = densities[x, y] @@ -64,17 +73,20 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta x_, y_ = x + xx, y + yy if 0 <= x_ < width and 0 <= y_ < height: if densities[x_, y_] > current_density: - dist = np.sum((current_pixel - image[x_, y_, :])**2) + (x - x_)**2 + (y - y_)**2 + dist = 0 + for c in xrange(channels): + dist += (current_pixel[c] - image[x_, y_, c])**2 + dist += (x - x_)**2 + (y - y_)**2 if dist < closest: closest = dist parent[x, y] = x_ * width + y_ dist_parent[x, y] = closest - dist_parent = dist_parent.ravel() + dist_parent_flat = dist_parent.ravel() flat = parent.ravel() - flat[dist_parent > tau] = np.arange(width * height)[dist_parent > tau] + flat[dist_parent_flat > tau] = np.arange(width * height)[dist_parent_flat > tau] old = np.zeros_like(flat) while (old != flat).any(): old = flat flat = flat[flat] - return flat.reshape(parent.shape) + return flat.reshape(width, height) From b977d59c1bd2cae4f91c49e23421f64c88972261 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 16 Jun 2012 23:21:44 +0200 Subject: [PATCH 155/648] Color example :) --- doc/examples/plot_quickshift.py | 52 ++++++++++------------------- skimage/segmentation/quickshift.pyx | 18 ++++++++-- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/doc/examples/plot_quickshift.py b/doc/examples/plot_quickshift.py index 80f216fa..08757e25 100644 --- a/doc/examples/plot_quickshift.py +++ b/doc/examples/plot_quickshift.py @@ -1,47 +1,31 @@ import matplotlib.pyplot as plt import numpy as np -from scipy import ndimage -#from skimage.data import lena -#from skimage.util import img_as_float +from skimage.data import lena from skimage.segmentation import quickshift +from skimage.util import img_as_float from IPython.core.debugger import Tracer tracer = Tracer() -def microstructure(l=256): - """ - Synthetic binary data: binary microstructure with blobs. - - Parameters - ---------- - - l: int, optional - linear size of the returned image - """ - n = 5 - x, y = np.ogrid[0:l, 0:l] - mask = np.zeros((l, l)) - generator = np.random.RandomState(1) - points = l * generator.rand(2, n ** 2) - mask[(points[0]).astype(np.int), (points[1]).astype(np.int)] = 1 - mask = ndimage.gaussian_filter(mask, sigma=l / (4. * n)) - return (mask > mask.mean()).astype(np.float) - - -#img = img_as_float(lena()[250:300, 250:300]) -img = microstructure(l=50) -segments = quickshift(img.reshape(50, 50, 1)) -segments = np.unique(segments, return_inverse=True)[1].reshape(50, 50) -intensities = np.bincount(segments.ravel(), img.ravel()) -counts = np.bincount(segments.ravel()) -intensities /= counts +img = img_as_float(lena())[::3, ::3, :].copy("C") +segments = quickshift(img, sigma=2) +segments = np.unique(segments, return_inverse=True)[1].reshape(img.shape[:2]) +plt.subplot(131, title="original") plt.imshow(img, interpolation='nearest') -plt.figure() -plt.imshow(segments, interpolation='nearest') -plt.figure() -plt.imshow(intensities[segments], interpolation='nearest') + +plt.subplot(132, title="superpixels") +# shuffle the labels for better visualization +permuted_labels = np.random.permutation(segments.max() + 1) +plt.imshow(permuted_labels[segments], interpolation='nearest') + +plt.subplot(133, title="mean color") +colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in + xrange(img.shape[2])] +counts = np.bincount(segments.ravel()) +colors = np.vstack(colors) / counts +plt.imshow(colors.T[segments], interpolation='nearest') plt.show() print("num segments: %d" % len(np.unique(segments))) diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index 5fd3eafe..3fbfbd98 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -3,11 +3,13 @@ cimport numpy as np from itertools import product +from time import time + cdef extern from "math.h": double exp(double) -def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, tau=10): +def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, tau=10, return_tree=False): """Computes quickshift clustering in RGB-(x,y) space. Parameters @@ -20,6 +22,8 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta tau: float Cut-off point for data distances. Higher means less clusters. + return_tree: bool + Whether to return the full segmentation hierarchy tree Returns ------- @@ -45,7 +49,7 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta cdef int x, y, xx, yy, x_, y_ cdef np.ndarray[dtype=np.float_t, ndim=2] densities = np.zeros((width, height)) - + start = time() # compute densities for x, y in product(xrange(width), xrange(height)): current_pixel = image[x, y, :] @@ -57,6 +61,7 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta dist += (current_pixel[c] - image[x_, y_, c])**2 dist += (x - x_)**2 + (y - y_)**2 densities[x, y] += float(exp(-dist / sigma)) + print("densities: %f" % (time() - start)) # this will break ties that otherwise would give us headache @@ -64,6 +69,7 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta # default parent to self: cdef np.ndarray[dtype=np.int_t, ndim=2] parent = np.arange(width * height).reshape(width, height) cdef np.ndarray[dtype=np.float_t, ndim=2] dist_parent = np.zeros((width, height)) + start = time() # find nearest node with higher density for x, y in product(xrange(width), xrange(height)): current_density = densities[x, y] @@ -81,7 +87,9 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta closest = dist parent[x, y] = x_ * width + y_ dist_parent[x, y] = closest + print("parents: %f" % (time() - start)) + start = time() dist_parent_flat = dist_parent.ravel() flat = parent.ravel() flat[dist_parent_flat > tau] = np.arange(width * height)[dist_parent_flat > tau] @@ -89,4 +97,8 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta while (old != flat).any(): old = flat flat = flat[flat] - return flat.reshape(width, height) + print("rest: %f" % (time() - start)) + flat = flat.reshape(width, height) + if return_tree: + return flat, parent + return flat From be4b44bc63a14b554ed80875ea00d2e8cae97666 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 16 Jun 2012 23:39:48 +0200 Subject: [PATCH 156/648] ENH CRAZY speedup --- doc/examples/plot_quickshift.py | 4 ++-- skimage/segmentation/quickshift.pyx | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/doc/examples/plot_quickshift.py b/doc/examples/plot_quickshift.py index 08757e25..59850469 100644 --- a/doc/examples/plot_quickshift.py +++ b/doc/examples/plot_quickshift.py @@ -9,8 +9,8 @@ from IPython.core.debugger import Tracer tracer = Tracer() -img = img_as_float(lena())[::3, ::3, :].copy("C") -segments = quickshift(img, sigma=2) +img = img_as_float(lena())[::2, ::2, :].copy("C") +segments = quickshift(img) segments = np.unique(segments, return_inverse=True)[1].reshape(img.shape[:2]) plt.subplot(131, title="original") diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index 3fbfbd98..4a619fc2 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -47,20 +47,26 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta cdef int channels = image.shape[2] cdef float closest, dist cdef int x, y, xx, yy, x_, y_ + + cdef np.float_t* image_p = image.data + cdef np.float_t* current_pixel_p = image_p + cdef np.float_t* current_entry_p cdef np.ndarray[dtype=np.float_t, ndim=2] densities = np.zeros((width, height)) start = time() # compute densities for x, y in product(xrange(width), xrange(height)): - current_pixel = image[x, y, :] for xx, yy in product(xrange(-w / 2, w / 2 + 1), repeat=2): x_, y_ = x + xx, y + yy if 0 <= x_ < width and 0 <= y_ < height: dist = 0 + current_entry_p = current_pixel_p for c in xrange(channels): - dist += (current_pixel[c] - image[x_, y_, c])**2 + dist += (current_pixel_p[c] - image[x_, y_, c])**2 dist += (x - x_)**2 + (y - y_)**2 densities[x, y] += float(exp(-dist / sigma)) + current_pixel_p += channels + print("densities: %f" % (time() - start)) # this will break ties that otherwise would give us headache @@ -71,9 +77,9 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta cdef np.ndarray[dtype=np.float_t, ndim=2] dist_parent = np.zeros((width, height)) start = time() # find nearest node with higher density + current_pixel_p = image_p for x, y in product(xrange(width), xrange(height)): current_density = densities[x, y] - current_pixel = image[x, y, :] closest = np.inf for xx, yy in product(xrange(-w / 2, w / 2 + 1), repeat=2): x_, y_ = x + xx, y + yy @@ -81,12 +87,13 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta if densities[x_, y_] > current_density: dist = 0 for c in xrange(channels): - dist += (current_pixel[c] - image[x_, y_, c])**2 + dist += (current_pixel_p[c] - image[x_, y_, c])**2 dist += (x - x_)**2 + (y - y_)**2 if dist < closest: closest = dist parent[x, y] = x_ * width + y_ dist_parent[x, y] = closest + current_pixel_p += channels print("parents: %f" % (time() - start)) start = time() From cb3dba7847f8e01ed3de2e9b94082e8ad944c8c9 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 17 Jun 2012 00:19:35 +0200 Subject: [PATCH 157/648] Bigger example --- doc/examples/plot_quickshift.py | 4 ++-- skimage/segmentation/quickshift.pyx | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/examples/plot_quickshift.py b/doc/examples/plot_quickshift.py index 59850469..b21c56e8 100644 --- a/doc/examples/plot_quickshift.py +++ b/doc/examples/plot_quickshift.py @@ -9,8 +9,8 @@ from IPython.core.debugger import Tracer tracer = Tracer() -img = img_as_float(lena())[::2, ::2, :].copy("C") -segments = quickshift(img) +img = img_as_float(lena()) +segments = quickshift(img, sigma=5, tau=20) segments = np.unique(segments, return_inverse=True)[1].reshape(img.shape[:2]) plt.subplot(131, title="original") diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index 4a619fc2..7e27fc56 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -33,6 +33,7 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta # We compute the distances twice since otherwise # we might get crazy memory overhead (width * height * windowsize**2) + # if you want to speed up things: computing exp in C is the bottleneck ;) # TODO do smoothing beforehand? # TODO manage borders somehow? @@ -64,7 +65,7 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta for c in xrange(channels): dist += (current_pixel_p[c] - image[x_, y_, c])**2 dist += (x - x_)**2 + (y - y_)**2 - densities[x, y] += float(exp(-dist / sigma)) + densities[x, y] += exp(-dist / sigma) current_pixel_p += channels print("densities: %f" % (time() - start)) From 58237a558a457ef7a32c67b99c2f81aa2b04e89a Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 17 Jun 2012 20:52:45 +0200 Subject: [PATCH 158/648] ENH dirty fix, works though. Starting profiling. --- skimage/segmentation/__init__.py | 4 +-- skimage/segmentation/felzenszwalb.pyx | 52 +++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 0ea91444..8fa8dfb8 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1,5 +1,5 @@ from .random_walker_segmentation import random_walker -#from .felzenszwalb import felzenszwalb_segmentation +from .felzenszwalb import felzenszwalb_segmentation from .quickshift import quickshift -__all__ = [random_walker, quickshift] +__all__ = [random_walker, quickshift, felzenszwalb_segmentation] diff --git a/skimage/segmentation/felzenszwalb.pyx b/skimage/segmentation/felzenszwalb.pyx index a53bf70e..11f3e93b 100644 --- a/skimage/segmentation/felzenszwalb.pyx +++ b/skimage/segmentation/felzenszwalb.pyx @@ -5,7 +5,49 @@ import scipy #from ..util import img_as_float #from ..color import rgb2grey -from skimage.morphology.ccomp cimport find_root, join_trees +#from skimage.morphology.ccomp cimport find_root, join_trees + +DTYPE = np.int +ctypedef np.int_t DTYPE_t + +cdef DTYPE_t find_root(np.int_t *forest, np.int_t n): + """Find the root of node n. + + """ + cdef np.int_t root = n + while (forest[root] < root): + root = forest[root] + return root + +cdef set_root(np.int_t *forest, np.int_t n, np.int_t root): + """ + Set all nodes on a path to point to new_root. + + """ + cdef np.int_t j + while (forest[n] < n): + j = forest[n] + forest[n] = root + n = j + + forest[n] = root + + +cdef join_trees(np.int_t *forest, np.int_t n, np.int_t m): + """Join two trees containing nodes n and m. + + """ + cdef np.int_t root = find_root(forest, n) + cdef np.int_t root_m + + if (n != m): + root_m = find_root(forest, m) + + if (root > root_m): + root = root_m + + set_root(forest, n, root) + set_root(forest, m, root) def felzenszwalb_segmentation(image, k, sigma=0.8): @@ -37,7 +79,7 @@ def felzenszwalb_segmentation(image, k, sigma=0.8): edge_queue = np.argsort(costs) cdef np.int_t *segments_p = segments.data cdef np.int_t seg_new - segment_size = defaultdict(lambda: 1) + cdef np.ndarray[np.int_t, ndim=1] segment_size = np.ones(width * height, dtype=np.int) # inner cost of segments cint = defaultdict(lambda: 0) for edge, cost in zip(edges[edge_queue], costs[edge_queue]): @@ -53,4 +95,8 @@ def felzenszwalb_segmentation(image, k, sigma=0.8): seg_new = find_root(segments_p, seg0) segment_size[seg_new] = segment_size[seg0] + segment_size[seg1] cint[seg_new] = cost - return segments + # unravel the union find tree + old = np.zeros_like(flat) + while (old != flat).any(): + old = flat + flat = flat[flat] From 888d176034369ad47b7333366a6a559228bdef8c Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 17 Jun 2012 20:56:50 +0200 Subject: [PATCH 159/648] ENH much faster. --- skimage/segmentation/felzenszwalb.pyx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/felzenszwalb.pyx b/skimage/segmentation/felzenszwalb.pyx index 11f3e93b..8a927fa0 100644 --- a/skimage/segmentation/felzenszwalb.pyx +++ b/skimage/segmentation/felzenszwalb.pyx @@ -78,10 +78,11 @@ def felzenszwalb_segmentation(image, k, sigma=0.8): # and inner cost, then start greedy iteration over edges. edge_queue = np.argsort(costs) cdef np.int_t *segments_p = segments.data - cdef np.int_t seg_new cdef np.ndarray[np.int_t, ndim=1] segment_size = np.ones(width * height, dtype=np.int) # inner cost of segments - cint = defaultdict(lambda: 0) + cdef np.ndarray[np.float_t, ndim=1] cint = np.zeros(width * height) + cdef int seg0, seg1, seg_new + cdef float cost, inner_cost0, inner_cost1 for edge, cost in zip(edges[edge_queue], costs[edge_queue]): seg0 = find_root(segments_p, edge[0]) seg1 = find_root(segments_p, edge[1]) @@ -100,3 +101,4 @@ def felzenszwalb_segmentation(image, k, sigma=0.8): while (old != flat).any(): old = flat flat = flat[flat] + return flat.reshape((width, height)) From 461d4be549b051dda2ceac8582ec69663c636740 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 17 Jun 2012 20:57:10 +0200 Subject: [PATCH 160/648] forgot a line :-/ --- skimage/segmentation/felzenszwalb.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/segmentation/felzenszwalb.pyx b/skimage/segmentation/felzenszwalb.pyx index 8a927fa0..6b3fd90a 100644 --- a/skimage/segmentation/felzenszwalb.pyx +++ b/skimage/segmentation/felzenszwalb.pyx @@ -97,6 +97,7 @@ def felzenszwalb_segmentation(image, k, sigma=0.8): segment_size[seg_new] = segment_size[seg0] + segment_size[seg1] cint[seg_new] = cost # unravel the union find tree + flat = segments.ravel() old = np.zeros_like(flat) while (old != flat).any(): old = flat From 0c198998256138be9bb034b1203d320bb3734c9e Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 17 Jun 2012 21:26:45 +0200 Subject: [PATCH 161/648] enh cythonizing some arrays --- skimage/segmentation/felzenszwalb.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/segmentation/felzenszwalb.pyx b/skimage/segmentation/felzenszwalb.pyx index 6b3fd90a..1275fa6a 100644 --- a/skimage/segmentation/felzenszwalb.pyx +++ b/skimage/segmentation/felzenszwalb.pyx @@ -64,8 +64,8 @@ def felzenszwalb_segmentation(image, k, sigma=0.8): down_cost = np.abs((image[:, 1:] - image[:, :-1])) dright_cost = np.abs((image[1:, 1:] - image[:-1, :-1])) uright_cost = np.abs((image[1:, :-1] - image[:-1, 1:])) - costs = np.hstack([right_cost.ravel(), down_cost.ravel(), - dright_cost.ravel(), uright_cost.ravel()]) + cdef np.ndarray[np.float_t, ndim=1] costs = np.hstack([right_cost.ravel(), down_cost.ravel(), + dright_cost.ravel(), uright_cost.ravel()]).astype(np.float) # compute edges between pixels: width, height = image.shape[:2] cdef np.ndarray[np.int_t, ndim=2] segments = np.arange(width * height).reshape(width, height) @@ -73,7 +73,7 @@ def felzenszwalb_segmentation(image, k, sigma=0.8): down_edges = np.c_[segments[:, 1:].ravel(), segments[:, :-1].ravel()] dright_edges = np.c_[segments[1:, 1:].ravel(), segments[:-1, :-1].ravel()] uright_edges = np.c_[segments[:-1, 1:].ravel(), segments[1:, :-1].ravel()] - edges = np.vstack([right_edges, down_edges, dright_edges, uright_edges]) + cdef np.ndarray[np.int_t, ndim=2] edges = np.vstack([right_edges, down_edges, dright_edges, uright_edges]) # initialize data structures for segment size # and inner cost, then start greedy iteration over edges. edge_queue = np.argsort(costs) From 7a5e7e49eac35894a86c05099d7af2dc27d9b616 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 17 Jun 2012 21:39:31 +0200 Subject: [PATCH 162/648] ENH reasonable speed for felzenszwalbs's segmentation --- skimage/segmentation/felzenszwalb.pyx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/skimage/segmentation/felzenszwalb.pyx b/skimage/segmentation/felzenszwalb.pyx index 1275fa6a..d45adeb3 100644 --- a/skimage/segmentation/felzenszwalb.pyx +++ b/skimage/segmentation/felzenszwalb.pyx @@ -3,6 +3,7 @@ cimport numpy as np from collections import defaultdict import scipy + #from ..util import img_as_float #from ..color import rgb2grey #from skimage.morphology.ccomp cimport find_root, join_trees @@ -58,8 +59,6 @@ def felzenszwalb_segmentation(image, k, sigma=0.8): image = scipy.ndimage.gaussian_filter(image, sigma=sigma) # compute edge weights in 8 connectivity: - #right_cost = np.sum((image[1:, :, :] - image[:-1, :, :]) ** 2, axis=2) - #down_cost = np.sum((image[:, 1:, :] - image[:, :-1, :]) ** 2, axis=2) right_cost = np.abs((image[1:, :] - image[:-1, :])) down_cost = np.abs((image[:, 1:] - image[:, :-1])) dright_cost = np.abs((image[1:, 1:] - image[:-1, :-1])) @@ -77,25 +76,35 @@ def felzenszwalb_segmentation(image, k, sigma=0.8): # initialize data structures for segment size # and inner cost, then start greedy iteration over edges. edge_queue = np.argsort(costs) + edges = np.ascontiguousarray(edges[edge_queue]) + costs = np.ascontiguousarray(costs[edge_queue]) cdef np.int_t *segments_p = segments.data + cdef np.int_t *edges_p = edges.data + cdef np.float_t *costs_p = costs.data cdef np.ndarray[np.int_t, ndim=1] segment_size = np.ones(width * height, dtype=np.int) # inner cost of segments cdef np.ndarray[np.float_t, ndim=1] cint = np.zeros(width * height) cdef int seg0, seg1, seg_new cdef float cost, inner_cost0, inner_cost1 - for edge, cost in zip(edges[edge_queue], costs[edge_queue]): - seg0 = find_root(segments_p, edge[0]) - seg1 = find_root(segments_p, edge[1]) + # set costs_p back one. we increase it before we use it + # since we might continue before that. + costs_p -= 1 + for e in xrange(costs.size): + seg0 = find_root(segments_p, edges_p[0]) + seg1 = find_root(segments_p, edges_p[1]) + edges_p += 2 + costs_p += 1 if seg0 == seg1: continue inner_cost0 = cint[seg0] + k / segment_size[seg0] inner_cost1 = cint[seg1] + k / segment_size[seg1] - if cost < min(inner_cost0, inner_cost1): + if costs_p[0] < min(inner_cost0, inner_cost1): # update size and cost join_trees(segments_p, seg0, seg1) seg_new = find_root(segments_p, seg0) segment_size[seg_new] = segment_size[seg0] + segment_size[seg1] - cint[seg_new] = cost + cint[seg_new] = costs_p[0] + # unravel the union find tree flat = segments.ravel() old = np.zeros_like(flat) From 07fb8d0c03cd9586feeeebddf2b3f5450d4254e1 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 17 Jun 2012 22:23:28 +0200 Subject: [PATCH 163/648] ENH felzenszwalb for color images --- .../plot_felzenszwalb_segmentation.py | 24 +++++-- .../{felzenszwalb.pyx => _felzenszwalb.pyx} | 33 +++++++-- skimage/segmentation/felzenszwalb.py | 67 +++++++++++++++++++ skimage/segmentation/setup.py | 4 +- 4 files changed, 114 insertions(+), 14 deletions(-) rename skimage/segmentation/{felzenszwalb.pyx => _felzenszwalb.pyx} (81%) create mode 100644 skimage/segmentation/felzenszwalb.py diff --git a/doc/examples/plot_felzenszwalb_segmentation.py b/doc/examples/plot_felzenszwalb_segmentation.py index dbcb2abb..0b8a2e8c 100644 --- a/doc/examples/plot_felzenszwalb_segmentation.py +++ b/doc/examples/plot_felzenszwalb_segmentation.py @@ -3,11 +3,25 @@ import numpy as np from skimage.data import lena from skimage.segmentation import felzenszwalb_segmentation +from skimage.util import img_as_float -img = lena() -segments = felzenszwalb_segmentation(img, k=1000) -plt.imshow(img) -plt.figure() -plt.imshow(segments) +img = img_as_float(lena()) +segments = felzenszwalb_segmentation(img, scale=1) +segments = np.unique(segments, return_inverse=True)[1].reshape(img.shape[:2]) + +plt.subplot(131, title="original") +plt.imshow(img, interpolation='nearest') + +plt.subplot(132, title="superpixels") +# shuffle the labels for better visualization +permuted_labels = np.random.permutation(segments.max() + 1) +plt.imshow(permuted_labels[segments], interpolation='nearest') + +plt.subplot(133, title="mean color") +colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in + xrange(img.shape[2])] +counts = np.bincount(segments.ravel()) +colors = np.vstack(colors) / counts +plt.imshow(colors.T[segments], interpolation='nearest') plt.show() print("num segments: %d" % len(np.unique(segments))) diff --git a/skimage/segmentation/felzenszwalb.pyx b/skimage/segmentation/_felzenszwalb.pyx similarity index 81% rename from skimage/segmentation/felzenszwalb.pyx rename to skimage/segmentation/_felzenszwalb.pyx index d45adeb3..cbdaac89 100644 --- a/skimage/segmentation/felzenszwalb.pyx +++ b/skimage/segmentation/_felzenszwalb.pyx @@ -51,11 +51,30 @@ cdef join_trees(np.int_t *forest, np.int_t n, np.int_t m): set_root(forest, m, root) -def felzenszwalb_segmentation(image, k, sigma=0.8): - k = float(k) - #image = img_as_float(image) - #image = rgb2grey(image) - image = image[:, :, 0] +def felzenszwalb_segmentation_grey(image, scale=200, sigma=0.8): + """Computes Felsenszwalb's efficient graph based segmentation for a single channel. + + Parameters + ---------- + image: ndarray, [width, height] + Input image + + scale: float + Free parameter. Higher means larger clusters. + For 0-255 data, hundereds are good. + + sigma: float + Width of Gaussian kernel used in preprocessing. + + Returns + ------- + segment_mask: ndarray, [width, height] + Integer mask indicating segment labels. + """ + if image.ndim != 2: + raise ValueError("This algorithm works only on single-channel 2d images." + "Got image of shape %s" % str(image.shape)) + scale = float(scale) image = scipy.ndimage.gaussian_filter(image, sigma=sigma) # compute edge weights in 8 connectivity: @@ -96,8 +115,8 @@ def felzenszwalb_segmentation(image, k, sigma=0.8): costs_p += 1 if seg0 == seg1: continue - inner_cost0 = cint[seg0] + k / segment_size[seg0] - inner_cost1 = cint[seg1] + k / segment_size[seg1] + inner_cost0 = cint[seg0] + scale / segment_size[seg0] + inner_cost1 = cint[seg1] + scale / segment_size[seg1] if costs_p[0] < min(inner_cost0, inner_cost1): # update size and cost join_trees(segments_p, seg0, seg1) diff --git a/skimage/segmentation/felzenszwalb.py b/skimage/segmentation/felzenszwalb.py new file mode 100644 index 00000000..6f2f89fc --- /dev/null +++ b/skimage/segmentation/felzenszwalb.py @@ -0,0 +1,67 @@ +import warnings +import numpy as np + +from ._felzenszwalb import felzenszwalb_segmentation_grey + +from IPython.core.debugger import Tracer +tracer = Tracer() + + +def felzenszwalb_segmentation(image, scale=200, sigma=0.8): + """Computes Felsenszwalb's segmentation for multi channel images. + + Calls the algorithm on each channel separately, then combines + using "and", i.e. two pixels are in the same segment if they are + in the same segment for each channel. + + Parameters + ---------- + image: ndarray, [width, height] + Input image + + scale: float + Free parameter. Higher means larger clusters. + For 0-255 data, hundereds are good. + + sigma: float + Width of Gaussian kernel used in preprocessing. + + Returns + ------- + segment_mask: ndarray, [width, height] + Integer mask indicating segment labels. + """ + + #image = img_as_float(image) + if image.ndim == 2: + # assume single channel image + return felzenszwalb_segmentation_grey(image, scale=scale, sigma=sigma) + + elif image.ndim != 3: + raise ValueError("Got image with ndim=%d, don't know" + " what to do." % image.ndim) + + # assume we got 2d image with multiple channels + n_channels = image.shape[2] + if n_channels != 3: + warnings.warn("Got image with %d channels. Is that really what you" + " wanted?" % image.shape[2]) + segmentations = [] + # compute quickshift for each channel + for c in xrange(n_channels): + channel = np.ascontiguousarray(image[:, :, c]) + seg = felzenszwalb_segmentation_grey(channel, scale=scale, sigma=sigma) + segmentations.append(seg) + + # put pixels in same segment only if in the same segment in all images + # we do this by combining the channels to one number + segmentations = [np.unique(s, return_inverse=True)[1] for s in + segmentations] + n0 = max(segmentations[0]) + n1 = max(segmentations[1]) + hasher = np.array([n1 * n0, n0, 1]) + segmentations = np.dstack(segmentations).reshape(-1, n_channels) + segmentation = np.dot(segmentations, hasher) + # make segment labels consecutive numbers starting at 0 + labels = np.unique(segmentation, return_inverse=True)[1] + return labels.reshape(image.shape[:2]) diff --git a/skimage/segmentation/setup.py b/skimage/segmentation/setup.py index a197555f..18713881 100644 --- a/skimage/segmentation/setup.py +++ b/skimage/segmentation/setup.py @@ -11,8 +11,8 @@ def configuration(parent_package='', top_path=None): config = Configuration('segmentation', parent_package, top_path) - cython(['felzenszwalb.pyx'], working_path=base_path) - config.add_extension('felzenszwalb', sources=['felzenszwalb.c'], + cython(['_felzenszwalb.pyx'], working_path=base_path) + config.add_extension('_felzenszwalb', sources=['_felzenszwalb.c'], include_dirs=[get_numpy_include_dirs()]) cython(['quickshift.pyx'], working_path=base_path) config.add_extension('quickshift', sources=['quickshift.c'], From 80b439bb4aa2fbdd8c7590f064acc43805919a3d Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 17 Jun 2012 22:47:29 +0200 Subject: [PATCH 164/648] ENH Polish examples. --- .../plot_felzenszwalb_segmentation.py | 20 ++++++++++++- doc/examples/plot_quickshift.py | 30 +++++++++++++++---- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/doc/examples/plot_felzenszwalb_segmentation.py b/doc/examples/plot_felzenszwalb_segmentation.py index 0b8a2e8c..27581fb1 100644 --- a/doc/examples/plot_felzenszwalb_segmentation.py +++ b/doc/examples/plot_felzenszwalb_segmentation.py @@ -1,3 +1,21 @@ +""" +================================================= +Felzenszwalb's efficient graph based segmentation +================================================= + +This fast 2d image segmentation algorithm, proposed in [1]_ is popular in the +computer vision community. It is often used to extract "superpixels", small +homogeneous image regions, which build the basis for further processing. + +The algorithm has a single ``scale`` parameter that influences the segment +size. The actual size and number of segments can vary greatly, depending on +local contrast. + +.. [1] Efficient graph-based image segmentation, Felzenszwalb, P.F. and + Huttenlocher, D.P. International Journal of Computer Vision, 2004 +""" +print __doc__ + import matplotlib.pyplot as plt import numpy as np @@ -23,5 +41,5 @@ colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in counts = np.bincount(segments.ravel()) colors = np.vstack(colors) / counts plt.imshow(colors.T[segments], interpolation='nearest') +print("number of segments: %d" % len(np.unique(segments))) plt.show() -print("num segments: %d" % len(np.unique(segments))) diff --git a/doc/examples/plot_quickshift.py b/doc/examples/plot_quickshift.py index b21c56e8..29f44899 100644 --- a/doc/examples/plot_quickshift.py +++ b/doc/examples/plot_quickshift.py @@ -1,3 +1,26 @@ +""" +============================= +Quickshift image segmentation +============================= + +Quickshift is a relatively recent 2d image segmentation algorithm, based on an +approximation of kernelized mean-shift. Therefore it belongs to the family +of local mode-seeking algorithms and is applied to the color+coordinate space, +see [1]_ It is often used to extract "superpixels", small homogeneous image +regions, which build the basis for further processing. + +One of the benefits of quickshift is that it actually computes a +hierarchical segmentation on multiple scales simultaneously. + +Quickshift has two parameters, one controlling the scale of the local +density approximation, the other selecting a level in the hierarchical +segmentation that is produced. + +.. [1] Quick shift and kernel methods for mode seeking, Vedaldi, A. and Soatto, S. + European Conference on Computer Vision, 2008 +""" +print __doc__ + import matplotlib.pyplot as plt import numpy as np @@ -5,11 +28,8 @@ from skimage.data import lena from skimage.segmentation import quickshift from skimage.util import img_as_float -from IPython.core.debugger import Tracer -tracer = Tracer() - -img = img_as_float(lena()) +img = img_as_float(lena())[::2, ::2, :].copy("C") segments = quickshift(img, sigma=5, tau=20) segments = np.unique(segments, return_inverse=True)[1].reshape(img.shape[:2]) @@ -27,5 +47,5 @@ colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in counts = np.bincount(segments.ravel()) colors = np.vstack(colors) / counts plt.imshow(colors.T[segments], interpolation='nearest') +print("number of segments: %d" % len(np.unique(segments))) plt.show() -print("num segments: %d" % len(np.unique(segments))) From 83616f0254bf33db6f886cdce2ebe1602c356d0a Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 17 Jun 2012 23:01:24 +0200 Subject: [PATCH 165/648] DOC more docs.... --- skimage/segmentation/felzenszwalb.py | 18 +++++++++++++++--- skimage/segmentation/quickshift.pyx | 23 ++++++++++++++++++----- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/skimage/segmentation/felzenszwalb.py b/skimage/segmentation/felzenszwalb.py index 6f2f89fc..b0467d57 100644 --- a/skimage/segmentation/felzenszwalb.py +++ b/skimage/segmentation/felzenszwalb.py @@ -3,13 +3,20 @@ import numpy as np from ._felzenszwalb import felzenszwalb_segmentation_grey -from IPython.core.debugger import Tracer -tracer = Tracer() - def felzenszwalb_segmentation(image, scale=200, sigma=0.8): """Computes Felsenszwalb's segmentation for multi channel images. + Produces an oversegmentation of a multichannel (i.e. RGB) image + using a fast, minimum spanning tree based clustering on the image grid. + The parameter ``scale`` sets an observation level. Higher scale means + less and larger segments. ``sigma`` is the diameter of a Gaussian kernel, + used for smoothing the image prior to segmentation. + + The number of produced segments as well as their size can only be + controlled indirectly through ``scale``. Segment size within an image can + vary greatly depending on local contrast. + Calls the algorithm on each channel separately, then combines using "and", i.e. two pixels are in the same segment if they are in the same segment for each channel. @@ -30,6 +37,11 @@ def felzenszwalb_segmentation(image, scale=200, sigma=0.8): ------- segment_mask: ndarray, [width, height] Integer mask indicating segment labels. + + References + ---------- + .. [1] Efficient graph-based image segmentation, Felzenszwalb, P.F. and + Huttenlocher, D.P. International Journal of Computer Vision, 2004 """ #image = img_as_float(image) diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index 7e27fc56..b47e5caa 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -10,7 +10,9 @@ cdef extern from "math.h": def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, tau=10, return_tree=False): - """Computes quickshift clustering in RGB-(x,y) space. + """Segments image using quickshift clustering in Color-(x,y) space. + + Produces an oversegmentation of the image using the quickshift mode-seeking algorithm. Parameters ---------- @@ -29,26 +31,37 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta ------- segment_mask: ndarray, [width, height] Integer mask indicating segment labels. + + Notes + ----- + The authors advocate to convert the image to Lab color space prior to segmentation. + + References + ---------- + .. [1] Quick shift and kernel methods for mode seeking, Vedaldi, A. and Soatto, S. + European Conference on Computer Vision, 2008 + + """ # We compute the distances twice since otherwise - # we might get crazy memory overhead (width * height * windowsize**2) - # if you want to speed up things: computing exp in C is the bottleneck ;) + # we get crazy memory overhead (width * height * windowsize**2) # TODO do smoothing beforehand? # TODO manage borders somehow? + # TODO join orphant roots? # window size for neighboring pixels to consider if sigma < 1: raise ValueError("Sigma should be >= 1") cdef int w = int(2 * sigma) - + cdef int width = image.shape[0] cdef int height = image.shape[1] cdef int channels = image.shape[2] cdef float closest, dist cdef int x, y, xx, yy, x_, y_ - + cdef np.float_t* image_p = image.data cdef np.float_t* current_pixel_p = image_p cdef np.float_t* current_entry_p From 9a8cb483c4ee649fe0b8ef3f1ae684ec6f05f6aa Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 17 Jun 2012 23:06:45 +0200 Subject: [PATCH 166/648] misc remove profiling outputs from quickshift --- skimage/segmentation/quickshift.pyx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index b47e5caa..df4c14b3 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -3,7 +3,6 @@ cimport numpy as np from itertools import product -from time import time cdef extern from "math.h": double exp(double) @@ -67,7 +66,6 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta cdef np.float_t* current_entry_p cdef np.ndarray[dtype=np.float_t, ndim=2] densities = np.zeros((width, height)) - start = time() # compute densities for x, y in product(xrange(width), xrange(height)): for xx, yy in product(xrange(-w / 2, w / 2 + 1), repeat=2): @@ -81,15 +79,12 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta densities[x, y] += exp(-dist / sigma) current_pixel_p += channels - print("densities: %f" % (time() - start)) - # this will break ties that otherwise would give us headache densities += np.random.normal(scale=0.00001, size=(width, height)) # default parent to self: cdef np.ndarray[dtype=np.int_t, ndim=2] parent = np.arange(width * height).reshape(width, height) cdef np.ndarray[dtype=np.float_t, ndim=2] dist_parent = np.zeros((width, height)) - start = time() # find nearest node with higher density current_pixel_p = image_p for x, y in product(xrange(width), xrange(height)): @@ -108,9 +103,7 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta parent[x, y] = x_ * width + y_ dist_parent[x, y] = closest current_pixel_p += channels - print("parents: %f" % (time() - start)) - start = time() dist_parent_flat = dist_parent.ravel() flat = parent.ravel() flat[dist_parent_flat > tau] = np.arange(width * height)[dist_parent_flat > tau] @@ -118,7 +111,6 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta while (old != flat).any(): old = flat flat = flat[flat] - print("rest: %f" % (time() - start)) flat = flat.reshape(width, height) if return_tree: return flat, parent From 4d10749a0ec979da8a1d086dbc8314486b5d951f Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 17 Jun 2012 23:17:29 +0200 Subject: [PATCH 167/648] DOC document and export felzenszwalb_segmentation_grey, prettify plots for the web. --- doc/examples/plot_felzenszwalb_segmentation.py | 11 +++++++++-- doc/examples/plot_quickshift.py | 9 ++++++++- skimage/segmentation/__init__.py | 4 +++- skimage/segmentation/_felzenszwalb.pyx | 10 ++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/doc/examples/plot_felzenszwalb_segmentation.py b/doc/examples/plot_felzenszwalb_segmentation.py index 27581fb1..3d06dd2a 100644 --- a/doc/examples/plot_felzenszwalb_segmentation.py +++ b/doc/examples/plot_felzenszwalb_segmentation.py @@ -27,13 +27,17 @@ img = img_as_float(lena()) segments = felzenszwalb_segmentation(img, scale=1) segments = np.unique(segments, return_inverse=True)[1].reshape(img.shape[:2]) +print("number of segments: %d" % len(np.unique(segments))) + plt.subplot(131, title="original") plt.imshow(img, interpolation='nearest') +plt.axis("off") -plt.subplot(132, title="superpixels") +plt.subplot(132, title="segmentation") # shuffle the labels for better visualization permuted_labels = np.random.permutation(segments.max() + 1) plt.imshow(permuted_labels[segments], interpolation='nearest') +plt.axis("off") plt.subplot(133, title="mean color") colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in @@ -41,5 +45,8 @@ colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in counts = np.bincount(segments.ravel()) colors = np.vstack(colors) / counts plt.imshow(colors.T[segments], interpolation='nearest') -print("number of segments: %d" % len(np.unique(segments))) +plt.axis("off") + +plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, + bottom=0.02, left=0.02, right=0.98) plt.show() diff --git a/doc/examples/plot_quickshift.py b/doc/examples/plot_quickshift.py index 29f44899..d807ae14 100644 --- a/doc/examples/plot_quickshift.py +++ b/doc/examples/plot_quickshift.py @@ -33,13 +33,17 @@ img = img_as_float(lena())[::2, ::2, :].copy("C") segments = quickshift(img, sigma=5, tau=20) segments = np.unique(segments, return_inverse=True)[1].reshape(img.shape[:2]) +print("number of segments: %d" % len(np.unique(segments))) + plt.subplot(131, title="original") plt.imshow(img, interpolation='nearest') +plt.axis("off") plt.subplot(132, title="superpixels") # shuffle the labels for better visualization permuted_labels = np.random.permutation(segments.max() + 1) plt.imshow(permuted_labels[segments], interpolation='nearest') +plt.axis("off") plt.subplot(133, title="mean color") colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in @@ -47,5 +51,8 @@ colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in counts = np.bincount(segments.ravel()) colors = np.vstack(colors) / counts plt.imshow(colors.T[segments], interpolation='nearest') -print("number of segments: %d" % len(np.unique(segments))) +plt.axis("off") + +plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, + bottom=0.02, left=0.02, right=0.98) plt.show() diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 8fa8dfb8..36595240 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1,5 +1,7 @@ from .random_walker_segmentation import random_walker from .felzenszwalb import felzenszwalb_segmentation +from .felzenszwalb import felzenszwalb_segmentation_grey from .quickshift import quickshift -__all__ = [random_walker, quickshift, felzenszwalb_segmentation] +__all__ = [random_walker, quickshift, felzenszwalb_segmentation, + felzenszwalb_segmentation_grey] diff --git a/skimage/segmentation/_felzenszwalb.pyx b/skimage/segmentation/_felzenszwalb.pyx index cbdaac89..5a6db993 100644 --- a/skimage/segmentation/_felzenszwalb.pyx +++ b/skimage/segmentation/_felzenszwalb.pyx @@ -54,6 +54,16 @@ cdef join_trees(np.int_t *forest, np.int_t n, np.int_t m): def felzenszwalb_segmentation_grey(image, scale=200, sigma=0.8): """Computes Felsenszwalb's efficient graph based segmentation for a single channel. + Produces an oversegmentation of a 2d image using a fast, minimum spanning + tree based clustering on the image grid. The parameter ``scale`` sets an + observation level. Higher scale means less and larger segments. ``sigma`` + is the diameter of a Gaussian kernel, used for smoothing the image prior to + segmentation. + + The number of produced segments as well as their size can only be + controlled indirectly through ``scale``. Segment size within an image can + vary greatly depending on local contrast. + Parameters ---------- image: ndarray, [width, height] From ce26467ad4bba4404d8d2edc46eb3efd06500a40 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Mon, 18 Jun 2012 00:37:49 +0200 Subject: [PATCH 168/648] ENH: make quickshift more tolerant to input type, just convert to float. Also keep track of random seed for reproducable tests. Finally, do a unique on the output and add testing. --- doc/examples/plot_quickshift.py | 1 - skimage/segmentation/quickshift.pyx | 28 ++++++++--- skimage/segmentation/tests/test_quickshift.py | 50 +++++++++++++++++++ 3 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 skimage/segmentation/tests/test_quickshift.py diff --git a/doc/examples/plot_quickshift.py b/doc/examples/plot_quickshift.py index d807ae14..f0ace2d4 100644 --- a/doc/examples/plot_quickshift.py +++ b/doc/examples/plot_quickshift.py @@ -31,7 +31,6 @@ from skimage.util import img_as_float img = img_as_float(lena())[::2, ::2, :].copy("C") segments = quickshift(img, sigma=5, tau=20) -segments = np.unique(segments, return_inverse=True)[1].reshape(img.shape[:2]) print("number of segments: %d" % len(np.unique(segments))) diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index df4c14b3..35293ea6 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -3,12 +3,14 @@ cimport numpy as np from itertools import product +from ..util import img_as_float + cdef extern from "math.h": double exp(double) -def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, tau=10, return_tree=False): +def quickshift(image, sigma=5, tau=10, return_tree=False, random_seed=None): """Segments image using quickshift clustering in Color-(x,y) space. Produces an oversegmentation of the image using the quickshift mode-seeking algorithm. @@ -25,6 +27,8 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta Higher means less clusters. return_tree: bool Whether to return the full segmentation hierarchy tree + random_seed: None or int + Random seed used for breaking ties Returns ------- @@ -42,6 +46,13 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta """ + image = np.atleast_3d(image) + cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c = img_as_float(np.ascontiguousarray(image)) + + if random_seed is None: + random_state = np.random.RandomState() + else: + random_state = np.random.RandomState(random_seed) # We compute the distances twice since otherwise # we get crazy memory overhead (width * height * windowsize**2) @@ -55,13 +66,13 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta raise ValueError("Sigma should be >= 1") cdef int w = int(2 * sigma) - cdef int width = image.shape[0] - cdef int height = image.shape[1] - cdef int channels = image.shape[2] + cdef int width = image_c.shape[0] + cdef int height = image_c.shape[1] + cdef int channels = image_c.shape[2] cdef float closest, dist cdef int x, y, xx, yy, x_, y_ - cdef np.float_t* image_p = image.data + cdef np.float_t* image_p = image_c.data cdef np.float_t* current_pixel_p = image_p cdef np.float_t* current_entry_p @@ -74,14 +85,14 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta dist = 0 current_entry_p = current_pixel_p for c in xrange(channels): - dist += (current_pixel_p[c] - image[x_, y_, c])**2 + dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 dist += (x - x_)**2 + (y - y_)**2 densities[x, y] += exp(-dist / sigma) current_pixel_p += channels # this will break ties that otherwise would give us headache - densities += np.random.normal(scale=0.00001, size=(width, height)) + densities += random_state.normal(scale=0.00001, size=(width, height)) # default parent to self: cdef np.ndarray[dtype=np.int_t, ndim=2] parent = np.arange(width * height).reshape(width, height) cdef np.ndarray[dtype=np.float_t, ndim=2] dist_parent = np.zeros((width, height)) @@ -96,7 +107,7 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta if densities[x_, y_] > current_density: dist = 0 for c in xrange(channels): - dist += (current_pixel_p[c] - image[x_, y_, c])**2 + dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 dist += (x - x_)**2 + (y - y_)**2 if dist < closest: closest = dist @@ -111,6 +122,7 @@ def quickshift(np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image, sigma=5, ta while (old != flat).any(): old = flat flat = flat[flat] + flat = np.unique(flat, return_inverse=True)[1] flat = flat.reshape(width, height) if return_tree: return flat, parent diff --git a/skimage/segmentation/tests/test_quickshift.py b/skimage/segmentation/tests/test_quickshift.py new file mode 100644 index 00000000..b1d17837 --- /dev/null +++ b/skimage/segmentation/tests/test_quickshift.py @@ -0,0 +1,50 @@ +import numpy as np +from numpy.testing import assert_equal, assert_array_equal +from nose.tools import assert_true, assert_greater +from skimage.segmentation import quickshift + + +def test_grey(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 20)) + img[:10, :10] = 0.2 + img[10:, :10] = 0.4 + img[10:, 10:] = 0.6 + img += 0.1 * rnd.normal(size=img.shape) + seg = quickshift(img, random_seed=0) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + # that mostly respect the 4 regions: + for i in xrange(4): + hist = np.histogram(img[seg == i], bins=[0, 0.1, 0.3, 0.5, 1])[0] + assert_greater(hist[i], 40) + + +def test_color(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 20, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + img += 0.2 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = quickshift(img, random_seed=0) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + assert_array_equal(seg[:10, :10], 0) + assert_array_equal(seg[10:, :10], 3) + assert_array_equal(seg[:10, 10:], 1) + assert_array_equal(seg[10:, 10:], 2) + + seg2 = quickshift(img, sigma=1, tau=3, random_seed=0) + # very oversegmented: + assert_equal(len(np.unique(seg2)), 30) + # still don't cross lines + assert_true((seg2[9, :] != seg2[10, :]).all()) + assert_true((seg2[:, 9] != seg2[:, 10]).all()) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() From 8fa5427afc7d888e9080982401b1cd0376299256 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Mon, 18 Jun 2012 08:26:12 +0200 Subject: [PATCH 169/648] FIX build problem and cython problem resolved. --- skimage/morphology/ccomp.pyx | 1 - skimage/segmentation/_felzenszwalb.pyx | 47 +------------------------- 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/skimage/morphology/ccomp.pyx b/skimage/morphology/ccomp.pyx index a1d5f303..ffb2cf11 100644 --- a/skimage/morphology/ccomp.pyx +++ b/skimage/morphology/ccomp.pyx @@ -24,7 +24,6 @@ See also: # The term "forest" is used to indicate an array that stores one or more trees DTYPE = np.int -ctypedef np.int_t DTYPE_t cdef DTYPE_t find_root(np.int_t *forest, np.int_t n): """Find the root of node n. diff --git a/skimage/segmentation/_felzenszwalb.pyx b/skimage/segmentation/_felzenszwalb.pyx index 5a6db993..afa39ad8 100644 --- a/skimage/segmentation/_felzenszwalb.pyx +++ b/skimage/segmentation/_felzenszwalb.pyx @@ -3,52 +3,7 @@ cimport numpy as np from collections import defaultdict import scipy - -#from ..util import img_as_float -#from ..color import rgb2grey -#from skimage.morphology.ccomp cimport find_root, join_trees - -DTYPE = np.int -ctypedef np.int_t DTYPE_t - -cdef DTYPE_t find_root(np.int_t *forest, np.int_t n): - """Find the root of node n. - - """ - cdef np.int_t root = n - while (forest[root] < root): - root = forest[root] - return root - -cdef set_root(np.int_t *forest, np.int_t n, np.int_t root): - """ - Set all nodes on a path to point to new_root. - - """ - cdef np.int_t j - while (forest[n] < n): - j = forest[n] - forest[n] = root - n = j - - forest[n] = root - - -cdef join_trees(np.int_t *forest, np.int_t n, np.int_t m): - """Join two trees containing nodes n and m. - - """ - cdef np.int_t root = find_root(forest, n) - cdef np.int_t root_m - - if (n != m): - root_m = find_root(forest, m) - - if (root > root_m): - root = root_m - - set_root(forest, n, root) - set_root(forest, m, root) +from skimage.morphology.ccomp cimport find_root, join_trees def felzenszwalb_segmentation_grey(image, scale=200, sigma=0.8): From 08df2a5103b8ab038ceee654930dddb0cb68fc09 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Tue, 19 Jun 2012 21:22:48 +0200 Subject: [PATCH 170/648] enh: minor simplifications --- skimage/segmentation/quickshift.pyx | 46 +++++++++++++++-------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index 35293ea6..2b6bdf24 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -1,5 +1,6 @@ import numpy as np cimport numpy as np +cimport cython from itertools import product @@ -10,6 +11,9 @@ cdef extern from "math.h": double exp(double) +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) def quickshift(image, sigma=5, tau=10, return_tree=False, random_seed=None): """Segments image using quickshift clustering in Color-(x,y) space. @@ -70,24 +74,22 @@ def quickshift(image, sigma=5, tau=10, return_tree=False, random_seed=None): cdef int height = image_c.shape[1] cdef int channels = image_c.shape[2] cdef float closest, dist - cdef int x, y, xx, yy, x_, y_ + cdef int x, y, x_, y_ cdef np.float_t* image_p = image_c.data cdef np.float_t* current_pixel_p = image_p - cdef np.float_t* current_entry_p cdef np.ndarray[dtype=np.float_t, ndim=2] densities = np.zeros((width, height)) # compute densities for x, y in product(xrange(width), xrange(height)): - for xx, yy in product(xrange(-w / 2, w / 2 + 1), repeat=2): - x_, y_ = x + xx, y + yy - if 0 <= x_ < width and 0 <= y_ < height: - dist = 0 - current_entry_p = current_pixel_p - for c in xrange(channels): - dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 - dist += (x - x_)**2 + (y - y_)**2 - densities[x, y] += exp(-dist / sigma) + x_min, x_max = max(x - w, 0), min(x + w + 1, width) + y_min, y_max = max(y - w, 0), min(y + w + 1, height) + for x_, y_ in product(xrange(x_min, x_max), xrange(y_min, y_max)): + dist = 0 + for c in xrange(channels): + dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 + dist += (x - x_)**2 + (y - y_)**2 + densities[x, y] += exp(-dist / sigma) current_pixel_p += channels # this will break ties that otherwise would give us headache @@ -101,17 +103,17 @@ def quickshift(image, sigma=5, tau=10, return_tree=False, random_seed=None): for x, y in product(xrange(width), xrange(height)): current_density = densities[x, y] closest = np.inf - for xx, yy in product(xrange(-w / 2, w / 2 + 1), repeat=2): - x_, y_ = x + xx, y + yy - if 0 <= x_ < width and 0 <= y_ < height: - if densities[x_, y_] > current_density: - dist = 0 - for c in xrange(channels): - dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 - dist += (x - x_)**2 + (y - y_)**2 - if dist < closest: - closest = dist - parent[x, y] = x_ * width + y_ + x_min, x_max = max(x - w, 0), min(x + w + 1, width) + y_min, y_max = max(y - w, 0), min(y + w + 1, height) + for x_, y_ in product(xrange(x_min, x_max), xrange(y_min, y_max)): + if densities[x_, y_] > current_density: + dist = 0 + for c in xrange(channels): + dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 + dist += (x - x_)**2 + (y - y_)**2 + if dist < closest: + closest = dist + parent[x, y] = x_ * width + y_ dist_parent[x, y] = closest current_pixel_p += channels From f0a7212c4f710fb3798ecb3c1b8e6fe8e910495c Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Tue, 19 Jun 2012 21:56:51 +0200 Subject: [PATCH 171/648] ENH Rename parameters in quickshift, add "ratio" --- doc/examples/plot_quickshift.py | 3 +-- skimage/segmentation/quickshift.pyx | 19 +++++++++++-------- skimage/segmentation/tests/test_quickshift.py | 4 ++-- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/doc/examples/plot_quickshift.py b/doc/examples/plot_quickshift.py index f0ace2d4..1285ad28 100644 --- a/doc/examples/plot_quickshift.py +++ b/doc/examples/plot_quickshift.py @@ -28,9 +28,8 @@ from skimage.data import lena from skimage.segmentation import quickshift from skimage.util import img_as_float - img = img_as_float(lena())[::2, ::2, :].copy("C") -segments = quickshift(img, sigma=5, tau=20) +segments = quickshift(img, kernel_size=5, max_dist=20) print("number of segments: %d" % len(np.unique(segments))) diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index 2b6bdf24..1822055b 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -14,7 +14,7 @@ cdef extern from "math.h": @cython.boundscheck(False) @cython.wraparound(False) @cython.cdivision(True) -def quickshift(image, sigma=5, tau=10, return_tree=False, random_seed=None): +def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, random_seed=None): """Segments image using quickshift clustering in Color-(x,y) space. Produces an oversegmentation of the image using the quickshift mode-seeking algorithm. @@ -23,10 +23,13 @@ def quickshift(image, sigma=5, tau=10, return_tree=False, random_seed=None): ---------- image: ndarray, [width, height, channels] Input image - sigma: float + ratio: float, between 0 and 1. + Balances color-space proximity and image-space proximity. + Higher values give more weight to color-space. + kernel_size: float Width of Gaussian kernel used in smoothing the sample density. Higher means less clusters. - tau: float + max_dist: float Cut-off point for data distances. Higher means less clusters. return_tree: bool @@ -51,7 +54,7 @@ def quickshift(image, sigma=5, tau=10, return_tree=False, random_seed=None): """ image = np.atleast_3d(image) - cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c = img_as_float(np.ascontiguousarray(image)) + cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c = img_as_float(np.ascontiguousarray(image)) * ratio if random_seed is None: random_state = np.random.RandomState() @@ -66,9 +69,9 @@ def quickshift(image, sigma=5, tau=10, return_tree=False, random_seed=None): # TODO join orphant roots? # window size for neighboring pixels to consider - if sigma < 1: + if kernel_size < 1: raise ValueError("Sigma should be >= 1") - cdef int w = int(2 * sigma) + cdef int w = int(2 * kernel_size) cdef int width = image_c.shape[0] cdef int height = image_c.shape[1] @@ -89,7 +92,7 @@ def quickshift(image, sigma=5, tau=10, return_tree=False, random_seed=None): for c in xrange(channels): dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 dist += (x - x_)**2 + (y - y_)**2 - densities[x, y] += exp(-dist / sigma) + densities[x, y] += exp(-dist / kernel_size) current_pixel_p += channels # this will break ties that otherwise would give us headache @@ -119,7 +122,7 @@ def quickshift(image, sigma=5, tau=10, return_tree=False, random_seed=None): dist_parent_flat = dist_parent.ravel() flat = parent.ravel() - flat[dist_parent_flat > tau] = np.arange(width * height)[dist_parent_flat > tau] + flat[dist_parent_flat > max_dist] = np.arange(width * height)[dist_parent_flat > max_dist] old = np.zeros_like(flat) while (old != flat).any(): old = flat diff --git a/skimage/segmentation/tests/test_quickshift.py b/skimage/segmentation/tests/test_quickshift.py index b1d17837..a904837c 100644 --- a/skimage/segmentation/tests/test_quickshift.py +++ b/skimage/segmentation/tests/test_quickshift.py @@ -37,9 +37,9 @@ def test_color(): assert_array_equal(seg[:10, 10:], 1) assert_array_equal(seg[10:, 10:], 2) - seg2 = quickshift(img, sigma=1, tau=3, random_seed=0) + seg2 = quickshift(img, kernel_size=1, max_dist=3, random_seed=0) # very oversegmented: - assert_equal(len(np.unique(seg2)), 30) + assert_equal(len(np.unique(seg2)), 18) # still don't cross lines assert_true((seg2[9, :] != seg2[10, :]).all()) assert_true((seg2[:, 9] != seg2[:, 10]).all()) From a7c98cb67a4eaca083f86a6c6fd1964a90b05b2f Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Tue, 19 Jun 2012 22:32:44 +0200 Subject: [PATCH 172/648] Remove felzenszwalb_segmentation_gray again since it just complicates the interface. --- skimage/segmentation/__init__.py | 4 +--- skimage/segmentation/_felzenszwalb.pyx | 10 ++++++---- skimage/segmentation/felzenszwalb.py | 14 ++++++-------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 36595240..8fa8dfb8 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1,7 +1,5 @@ from .random_walker_segmentation import random_walker from .felzenszwalb import felzenszwalb_segmentation -from .felzenszwalb import felzenszwalb_segmentation_grey from .quickshift import quickshift -__all__ = [random_walker, quickshift, felzenszwalb_segmentation, - felzenszwalb_segmentation_grey] +__all__ = [random_walker, quickshift, felzenszwalb_segmentation] diff --git a/skimage/segmentation/_felzenszwalb.pyx b/skimage/segmentation/_felzenszwalb.pyx index afa39ad8..d058975d 100644 --- a/skimage/segmentation/_felzenszwalb.pyx +++ b/skimage/segmentation/_felzenszwalb.pyx @@ -1,12 +1,13 @@ import numpy as np cimport numpy as np -from collections import defaultdict import scipy from skimage.morphology.ccomp cimport find_root, join_trees +from ..util import img_as_float -def felzenszwalb_segmentation_grey(image, scale=200, sigma=0.8): + +def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8): """Computes Felsenszwalb's efficient graph based segmentation for a single channel. Produces an oversegmentation of a 2d image using a fast, minimum spanning @@ -26,7 +27,6 @@ def felzenszwalb_segmentation_grey(image, scale=200, sigma=0.8): scale: float Free parameter. Higher means larger clusters. - For 0-255 data, hundereds are good. sigma: float Width of Gaussian kernel used in preprocessing. @@ -39,6 +39,7 @@ def felzenszwalb_segmentation_grey(image, scale=200, sigma=0.8): if image.ndim != 2: raise ValueError("This algorithm works only on single-channel 2d images." "Got image of shape %s" % str(image.shape)) + image = img_as_float(image) scale = float(scale) image = scipy.ndimage.gaussian_filter(image, sigma=sigma) @@ -88,11 +89,12 @@ def felzenszwalb_segmentation_grey(image, scale=200, sigma=0.8): seg_new = find_root(segments_p, seg0) segment_size[seg_new] = segment_size[seg0] + segment_size[seg1] cint[seg_new] = costs_p[0] - + # unravel the union find tree flat = segments.ravel() old = np.zeros_like(flat) while (old != flat).any(): old = flat flat = flat[flat] + flat = np.unique(flat, return_inverse=True)[1] return flat.reshape((width, height)) diff --git a/skimage/segmentation/felzenszwalb.py b/skimage/segmentation/felzenszwalb.py index b0467d57..be879e13 100644 --- a/skimage/segmentation/felzenszwalb.py +++ b/skimage/segmentation/felzenszwalb.py @@ -1,11 +1,11 @@ import warnings import numpy as np -from ._felzenszwalb import felzenszwalb_segmentation_grey +from ._felzenszwalb import _felzenszwalb_segmentation_grey -def felzenszwalb_segmentation(image, scale=200, sigma=0.8): - """Computes Felsenszwalb's segmentation for multi channel images. +def felzenszwalb_segmentation(image, scale=1, sigma=0.8): + """Computes Felsenszwalb's efficient graph based image segmentation. Produces an oversegmentation of a multichannel (i.e. RGB) image using a fast, minimum spanning tree based clustering on the image grid. @@ -47,7 +47,7 @@ def felzenszwalb_segmentation(image, scale=200, sigma=0.8): #image = img_as_float(image) if image.ndim == 2: # assume single channel image - return felzenszwalb_segmentation_grey(image, scale=scale, sigma=sigma) + return _felzenszwalb_segmentation_grey(image, scale=scale, sigma=sigma) elif image.ndim != 3: raise ValueError("Got image with ndim=%d, don't know" @@ -62,13 +62,11 @@ def felzenszwalb_segmentation(image, scale=200, sigma=0.8): # compute quickshift for each channel for c in xrange(n_channels): channel = np.ascontiguousarray(image[:, :, c]) - seg = felzenszwalb_segmentation_grey(channel, scale=scale, sigma=sigma) - segmentations.append(seg) + s = _felzenszwalb_segmentation_grey(channel, scale=scale, sigma=sigma) + segmentations.append(s) # put pixels in same segment only if in the same segment in all images # we do this by combining the channels to one number - segmentations = [np.unique(s, return_inverse=True)[1] for s in - segmentations] n0 = max(segmentations[0]) n1 = max(segmentations[1]) hasher = np.array([n1 * n0, n0, 1]) From d2e226fe59ae1b9aa43207e10ea84d85ea160e7c Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Tue, 19 Jun 2012 22:33:14 +0200 Subject: [PATCH 173/648] ENH tests for Felzenszwalbs segmentation, fixed off-by-one error --- skimage/segmentation/felzenszwalb.py | 4 +- .../segmentation/tests/test_felzenszwalb.py | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 skimage/segmentation/tests/test_felzenszwalb.py diff --git a/skimage/segmentation/felzenszwalb.py b/skimage/segmentation/felzenszwalb.py index be879e13..54846102 100644 --- a/skimage/segmentation/felzenszwalb.py +++ b/skimage/segmentation/felzenszwalb.py @@ -67,8 +67,8 @@ def felzenszwalb_segmentation(image, scale=1, sigma=0.8): # put pixels in same segment only if in the same segment in all images # we do this by combining the channels to one number - n0 = max(segmentations[0]) - n1 = max(segmentations[1]) + n0 = segmentations[0].max() + 1 + n1 = segmentations[1].max() + 1 hasher = np.array([n1 * n0, n0, 1]) segmentations = np.dstack(segmentations).reshape(-1, n_channels) segmentation = np.dot(segmentations, hasher) diff --git a/skimage/segmentation/tests/test_felzenszwalb.py b/skimage/segmentation/tests/test_felzenszwalb.py new file mode 100644 index 00000000..d645ab9d --- /dev/null +++ b/skimage/segmentation/tests/test_felzenszwalb.py @@ -0,0 +1,39 @@ +import numpy as np +from numpy.testing import assert_equal, assert_array_equal +from nose.tools import assert_greater +from skimage.segmentation import felzenszwalb_segmentation + + +def test_grey(): + # very weak tests. This algorithm is pretty unstable. + img = np.zeros((20, 20)) + img[:10, 10:] = 0.2 + img[10:, :10] = 0.4 + img[10:, 10:] = 0.6 + seg = felzenszwalb_segmentation(img, sigma=0) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + # that mostly respect the 4 regions: + for i in xrange(4): + hist = np.histogram(img[seg == i], bins=[0, 0.1, 0.3, 0.5, 1])[0] + assert_greater(hist[i], 40) + + +def test_color(): + # very weak tests. This algorithm is pretty unstable. + img = np.zeros((20, 20, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + seg = felzenszwalb_segmentation(img, sigma=0) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + assert_array_equal(seg[:10, :10], 0) + assert_array_equal(seg[10:, :10], 3) + assert_array_equal(seg[:10, 10:], 1) + assert_array_equal(seg[10:, 10:], 2) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() From 05cc863f3f2090a1eb10f96ba185cd0c9225d2f8 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Tue, 19 Jun 2012 23:58:40 +0200 Subject: [PATCH 174/648] First draft for numpy based km_segmentation --- skimage/segmentation/__init__.py | 4 ++- skimage/segmentation/km_segmentation.py | 42 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 skimage/segmentation/km_segmentation.py diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 8fa8dfb8..2de27ad1 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1,5 +1,7 @@ from .random_walker_segmentation import random_walker from .felzenszwalb import felzenszwalb_segmentation +from .km_segmentation import km_segmentation from .quickshift import quickshift -__all__ = [random_walker, quickshift, felzenszwalb_segmentation] +__all__ = [random_walker, quickshift, felzenszwalb_segmentation, + km_segmentation] diff --git a/skimage/segmentation/km_segmentation.py b/skimage/segmentation/km_segmentation.py new file mode 100644 index 00000000..fa307ab3 --- /dev/null +++ b/skimage/segmentation/km_segmentation.py @@ -0,0 +1,42 @@ +import numpy as np + + +def km_segmentation(image, n_segments=100, ratio=50, max_iter=100): + # initialize on grid: + height, width = image.shape[:2] + # approximate grid size for desired n_segments + step = np.sqrt(height * width / n_segments) + grid_y, grid_x = np.mgrid[:height, :width] + means_y = grid_y[::step, ::step] + means_x = grid_x[::step, ::step] + + means_color = image[means_y, means_x, :] + means = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) + image = np.dstack([grid_y, grid_x, image * ratio]) + + nearest_mean = np.zeros((height, width), dtype=np.int) + distance = np.ones((height, width), dtype=np.float) * np.inf + for i in xrange(max_iter): + print("iteration %d" % i) + nearest_mean_old = nearest_mean.copy() + # assign pixels to means + for k, mean in enumerate(means): + # compute windows: + y_min = int(max(mean[0] - 2 * step, 0)) + y_max = int(min(mean[0] + 2 * step, height)) + x_min = int(max(mean[1] - 2 * step, 0)) + x_max = int(min(mean[1] + 2 * step, height)) + search_window = image[y_min:y_max + 1, x_min:x_max + 1] + dist_mean = np.sum((search_window - mean) ** 2, axis=2) + assign = distance[y_min:y_max + 1, x_min:x_max + 1] > dist_mean + nearest_mean[y_min:y_max + 1, x_min:x_max + 1][assign] = k + distance[y_min:y_max + 1, x_min:x_max + 1][assign] = \ + dist_mean[assign] + if (nearest_mean == nearest_mean_old).all(): + break + # recompute means: + means = [np.bincount(nearest_mean.ravel(), image[:, :, j].ravel()) + for j in xrange(5)] + in_mean = np.bincount(nearest_mean.ravel()) + means = (np.vstack(means) / in_mean).T + return nearest_mean From ccfb89b9570598f41cff7ad4d34f3236c1d911f3 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 22 Jun 2012 23:08:59 +0200 Subject: [PATCH 175/648] FIX Tried to address @stefanv's comments on the PR. --- .../plot_felzenszwalb_segmentation.py | 25 +++++++++---------- doc/examples/plot_quickshift.py | 24 ++++++++---------- skimage/segmentation/__init__.py | 2 +- skimage/segmentation/_felzenszwalb.pyx | 6 ++--- skimage/segmentation/felzenszwalb.py | 10 +++----- skimage/segmentation/quickshift.pyx | 4 +-- 6 files changed, 31 insertions(+), 40 deletions(-) diff --git a/doc/examples/plot_felzenszwalb_segmentation.py b/doc/examples/plot_felzenszwalb_segmentation.py index 3d06dd2a..18ac0f80 100644 --- a/doc/examples/plot_felzenszwalb_segmentation.py +++ b/doc/examples/plot_felzenszwalb_segmentation.py @@ -29,24 +29,23 @@ segments = np.unique(segments, return_inverse=True)[1].reshape(img.shape[:2]) print("number of segments: %d" % len(np.unique(segments))) -plt.subplot(131, title="original") -plt.imshow(img, interpolation='nearest') -plt.axis("off") -plt.subplot(132, title="segmentation") -# shuffle the labels for better visualization -permuted_labels = np.random.permutation(segments.max() + 1) -plt.imshow(permuted_labels[segments], interpolation='nearest') -plt.axis("off") +fig, (ax_org, ax_sp, ax_mean) = plt.subplots(1, 3) +ax_org.set_title("original") +ax_org.imshow(img, interpolation='nearest') +ax_org.axis("off") + +ax_sp.set_title("superpixels") +ax_sp.imshow(segments, interpolation='nearest', cmap=plt.cm.prism) +ax_sp.axis("off") -plt.subplot(133, title="mean color") colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in xrange(img.shape[2])] counts = np.bincount(segments.ravel()) colors = np.vstack(colors) / counts -plt.imshow(colors.T[segments], interpolation='nearest') -plt.axis("off") - -plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, +ax_mean.set_title("mean color") +ax_mean.imshow(colors.T[segments], interpolation='nearest') +ax_mean.axis("off") +fig.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, bottom=0.02, left=0.02, right=0.98) plt.show() diff --git a/doc/examples/plot_quickshift.py b/doc/examples/plot_quickshift.py index 1285ad28..5adf1969 100644 --- a/doc/examples/plot_quickshift.py +++ b/doc/examples/plot_quickshift.py @@ -33,24 +33,22 @@ segments = quickshift(img, kernel_size=5, max_dist=20) print("number of segments: %d" % len(np.unique(segments))) -plt.subplot(131, title="original") -plt.imshow(img, interpolation='nearest') -plt.axis("off") +fig, (ax_org, ax_sp, ax_mean) = plt.subplots(1, 3) +ax_org.set_title("original") +ax_org.imshow(img, interpolation='nearest') +ax_org.axis("off") -plt.subplot(132, title="superpixels") -# shuffle the labels for better visualization -permuted_labels = np.random.permutation(segments.max() + 1) -plt.imshow(permuted_labels[segments], interpolation='nearest') -plt.axis("off") +ax_sp.set_title("superpixels") +ax_sp.imshow(segments, interpolation='nearest', cmap=plt.cm.prism) +ax_sp.axis("off") -plt.subplot(133, title="mean color") colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in xrange(img.shape[2])] counts = np.bincount(segments.ravel()) colors = np.vstack(colors) / counts -plt.imshow(colors.T[segments], interpolation='nearest') -plt.axis("off") - -plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, +ax_mean.set_title("mean color") +ax_mean.imshow(colors.T[segments], interpolation='nearest') +ax_mean.axis("off") +fig.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, bottom=0.02, left=0.02, right=0.98) plt.show() diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 2de27ad1..b2c97448 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -4,4 +4,4 @@ from .km_segmentation import km_segmentation from .quickshift import quickshift __all__ = [random_walker, quickshift, felzenszwalb_segmentation, - km_segmentation] + km_segmentation] diff --git a/skimage/segmentation/_felzenszwalb.pyx b/skimage/segmentation/_felzenszwalb.pyx index d058975d..a677020a 100644 --- a/skimage/segmentation/_felzenszwalb.pyx +++ b/skimage/segmentation/_felzenszwalb.pyx @@ -22,12 +22,10 @@ def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8): Parameters ---------- - image: ndarray, [width, height] + image: (width, height) ndarray Input image - scale: float Free parameter. Higher means larger clusters. - sigma: float Width of Gaussian kernel used in preprocessing. @@ -74,7 +72,7 @@ def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8): # set costs_p back one. we increase it before we use it # since we might continue before that. costs_p -= 1 - for e in xrange(costs.size): + for e in range(costs.size): seg0 = find_root(segments_p, edges_p[0]) seg1 = find_root(segments_p, edges_p[1]) edges_p += 2 diff --git a/skimage/segmentation/felzenszwalb.py b/skimage/segmentation/felzenszwalb.py index 54846102..cfcf4f23 100644 --- a/skimage/segmentation/felzenszwalb.py +++ b/skimage/segmentation/felzenszwalb.py @@ -23,13 +23,10 @@ def felzenszwalb_segmentation(image, scale=1, sigma=0.8): Parameters ---------- - image: ndarray, [width, height] + image: (width, height) ndarray Input image - scale: float Free parameter. Higher means larger clusters. - For 0-255 data, hundereds are good. - sigma: float Width of Gaussian kernel used in preprocessing. @@ -69,9 +66,8 @@ def felzenszwalb_segmentation(image, scale=1, sigma=0.8): # we do this by combining the channels to one number n0 = segmentations[0].max() + 1 n1 = segmentations[1].max() + 1 - hasher = np.array([n1 * n0, n0, 1]) - segmentations = np.dstack(segmentations).reshape(-1, n_channels) - segmentation = np.dot(segmentations, hasher) + segmentation = (segmentations[0] + segmentations[1] * n0 + + segmentations[2] * n0 * n1) # make segment labels consecutive numbers starting at 0 labels = np.unique(segmentation, return_inverse=True)[1] return labels.reshape(image.shape[:2]) diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index 1822055b..b0a9b2d6 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -21,7 +21,7 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, r Parameters ---------- - image: ndarray, [width, height, channels] + image: (width, height, channels) ndarray Input image ratio: float, between 0 and 1. Balances color-space proximity and image-space proximity. @@ -54,7 +54,7 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, r """ image = np.atleast_3d(image) - cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c = img_as_float(np.ascontiguousarray(image)) * ratio + cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c = np.ascontiguousarray(img_as_float(image)) * ratio if random_seed is None: random_state = np.random.RandomState() From 49bc44c6e9686a1f8d3d9a5a62b458c11442264a Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 22 Jun 2012 23:10:58 +0200 Subject: [PATCH 176/648] FIXed test to work with the fixed "hashing" of colors --- skimage/segmentation/tests/test_felzenszwalb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/tests/test_felzenszwalb.py b/skimage/segmentation/tests/test_felzenszwalb.py index d645ab9d..f6cca31b 100644 --- a/skimage/segmentation/tests/test_felzenszwalb.py +++ b/skimage/segmentation/tests/test_felzenszwalb.py @@ -29,9 +29,9 @@ def test_color(): # we expect 4 segments: assert_equal(len(np.unique(seg)), 4) assert_array_equal(seg[:10, :10], 0) - assert_array_equal(seg[10:, :10], 3) + assert_array_equal(seg[10:, :10], 2) assert_array_equal(seg[:10, 10:], 1) - assert_array_equal(seg[10:, 10:], 2) + assert_array_equal(seg[10:, 10:], 3) if __name__ == '__main__': From a779952619d65307a8ec6e29f1e416a2eeab9688 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 22 Jun 2012 23:16:25 +0200 Subject: [PATCH 177/648] DOC updates CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index baaf064b..b00593c7 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -74,6 +74,7 @@ - Andreas Mueller Example data set loader. + Quickshift image segmentation, Felzenszwalbs fast graph based segmentation. - Yaroslav Halchenko For sharing his expert advice on Debian packaging. From 7b646ad7eabf4cfe356b327b697643ab62fd1da5 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 23 Jun 2012 01:07:32 +0200 Subject: [PATCH 178/648] fix trying to make this implementation more like slic --- skimage/segmentation/km_segmentation.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/km_segmentation.py b/skimage/segmentation/km_segmentation.py index fa307ab3..1c50a8da 100644 --- a/skimage/segmentation/km_segmentation.py +++ b/skimage/segmentation/km_segmentation.py @@ -1,7 +1,10 @@ import numpy as np +from scipy import ndimage +from ..util import img_as_float -def km_segmentation(image, n_segments=100, ratio=50, max_iter=100): +def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): + image = ndimage.gaussian_filter(img_as_float(image), sigma) # initialize on grid: height, width = image.shape[:2] # approximate grid size for desired n_segments @@ -12,7 +15,11 @@ def km_segmentation(image, n_segments=100, ratio=50, max_iter=100): means_color = image[means_y, means_x, :] means = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) - image = np.dstack([grid_y, grid_x, image * ratio]) + # we do the scaling of ratio in the same way as in the SLIC paper + # so the values have the same meaning + ratio = (ratio / float(step)) ** 2 + print(ratio) + image = np.dstack([grid_y, grid_x, image / ratio]) nearest_mean = np.zeros((height, width), dtype=np.int) distance = np.ones((height, width), dtype=np.float) * np.inf From 8f243667900e48da50069db4ce777c0aa8fb192e Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Mon, 2 Jul 2012 21:29:01 +0100 Subject: [PATCH 179/648] starting cython implementation of km_segmentation --- skimage/segmentation/km_segmentation.py | 49 --------------------- skimage/segmentation/km_segmentation.pyx | 55 ++++++++++++++++++++++++ skimage/segmentation/setup.py | 3 ++ 3 files changed, 58 insertions(+), 49 deletions(-) delete mode 100644 skimage/segmentation/km_segmentation.py create mode 100644 skimage/segmentation/km_segmentation.pyx diff --git a/skimage/segmentation/km_segmentation.py b/skimage/segmentation/km_segmentation.py deleted file mode 100644 index 1c50a8da..00000000 --- a/skimage/segmentation/km_segmentation.py +++ /dev/null @@ -1,49 +0,0 @@ -import numpy as np -from scipy import ndimage -from ..util import img_as_float - - -def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): - image = ndimage.gaussian_filter(img_as_float(image), sigma) - # initialize on grid: - height, width = image.shape[:2] - # approximate grid size for desired n_segments - step = np.sqrt(height * width / n_segments) - grid_y, grid_x = np.mgrid[:height, :width] - means_y = grid_y[::step, ::step] - means_x = grid_x[::step, ::step] - - means_color = image[means_y, means_x, :] - means = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) - # we do the scaling of ratio in the same way as in the SLIC paper - # so the values have the same meaning - ratio = (ratio / float(step)) ** 2 - print(ratio) - image = np.dstack([grid_y, grid_x, image / ratio]) - - nearest_mean = np.zeros((height, width), dtype=np.int) - distance = np.ones((height, width), dtype=np.float) * np.inf - for i in xrange(max_iter): - print("iteration %d" % i) - nearest_mean_old = nearest_mean.copy() - # assign pixels to means - for k, mean in enumerate(means): - # compute windows: - y_min = int(max(mean[0] - 2 * step, 0)) - y_max = int(min(mean[0] + 2 * step, height)) - x_min = int(max(mean[1] - 2 * step, 0)) - x_max = int(min(mean[1] + 2 * step, height)) - search_window = image[y_min:y_max + 1, x_min:x_max + 1] - dist_mean = np.sum((search_window - mean) ** 2, axis=2) - assign = distance[y_min:y_max + 1, x_min:x_max + 1] > dist_mean - nearest_mean[y_min:y_max + 1, x_min:x_max + 1][assign] = k - distance[y_min:y_max + 1, x_min:x_max + 1][assign] = \ - dist_mean[assign] - if (nearest_mean == nearest_mean_old).all(): - break - # recompute means: - means = [np.bincount(nearest_mean.ravel(), image[:, :, j].ravel()) - for j in xrange(5)] - in_mean = np.bincount(nearest_mean.ravel()) - means = (np.vstack(means) / in_mean).T - return nearest_mean diff --git a/skimage/segmentation/km_segmentation.pyx b/skimage/segmentation/km_segmentation.pyx new file mode 100644 index 00000000..53237618 --- /dev/null +++ b/skimage/segmentation/km_segmentation.pyx @@ -0,0 +1,55 @@ +import numpy as np +cimport numpy as np +from scipy import ndimage +from ..util import img_as_float + + +def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): + image = ndimage.gaussian_filter(img_as_float(image), sigma) + # initialize on grid: + height, width = image.shape[:2] + # approximate grid size for desired n_segments + step = np.sqrt(height * width / n_segments) + grid_y, grid_x = np.mgrid[:height, :width] + means_y = grid_y[::step, ::step] + means_x = grid_x[::step, ::step] + + means_color = image[means_y, means_x, :] + cdef np.ndarray[dtype=np.float_t, ndim=2] means = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) + n_means = means.shape[0] + # we do the scaling of ratio in the same way as in the SLIC paper + # so the values have the same meaning + ratio = (ratio / float(step)) ** 2 + print(ratio) + cdef np.ndarray[dtype=np.float_t, ndim=3] image_yx = np.dstack([grid_y, grid_x, image / ratio]) + cdef int i, k, x, y, x_min, x_max, y_min, y_max + cdef float dist_mean + + cdef np.ndarray[dtype=np.int_t, ndim=2] nearest_mean = np.zeros((height, width), dtype=np.int) + cdef np.ndarray[dtype=np.float_t, ndim=2] distance = np.ones((height, width), dtype=np.float) * np.inf + for i in xrange(max_iter): + print("iteration %d" % i) + nearest_mean_old = nearest_mean.copy() + # assign pixels to means + for k in xrange(n_means): + # compute windows: + y_min = int(max(means[k, 0] - 2 * step, 0)) + y_max = int(min(means[k, 0] + 2 * step, height)) + x_min = int(max(means[k, 1] - 2 * step, 0)) + x_max = int(min(means[k, 1] + 2 * step, height)) + for x in xrange(x_min, x_max): + for y in xrange(y_min, y_max): + dist_mean = 0 + for c in range(5): + dist_mean += (image_yx[y, x, c] - means[k, c]) ** 2 + if distance[y, x] > dist_mean: + nearest_mean[y, x] = k + distance[y, x] = dist_mean + if (nearest_mean == nearest_mean_old).all(): + break + # recompute means: + means_list = [np.bincount(nearest_mean.ravel(), image_yx[:, :, j].ravel()) + for j in xrange(5)] + in_mean = np.bincount(nearest_mean.ravel()) + means = (np.vstack(means_list) / in_mean).T + return nearest_mean diff --git a/skimage/segmentation/setup.py b/skimage/segmentation/setup.py index 18713881..0be6b748 100644 --- a/skimage/segmentation/setup.py +++ b/skimage/segmentation/setup.py @@ -17,6 +17,9 @@ def configuration(parent_package='', top_path=None): cython(['quickshift.pyx'], working_path=base_path) config.add_extension('quickshift', sources=['quickshift.c'], include_dirs=[get_numpy_include_dirs()]) + cython(['km_segmentation.pyx'], working_path=base_path) + config.add_extension('km_segmentation', sources=['km_segmentation.c'], + include_dirs=[get_numpy_include_dirs()]) return config From 501a6db8ad048ae25396ab6d4da50de475f29e52 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Mon, 2 Jul 2012 22:54:49 +0100 Subject: [PATCH 180/648] ENH speedup, means and image use pointers --- skimage/segmentation/km_segmentation.pyx | 34 +++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/skimage/segmentation/km_segmentation.pyx b/skimage/segmentation/km_segmentation.pyx index 53237618..2f1dae5d 100644 --- a/skimage/segmentation/km_segmentation.pyx +++ b/skimage/segmentation/km_segmentation.pyx @@ -16,40 +16,56 @@ def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): means_color = image[means_y, means_x, :] cdef np.ndarray[dtype=np.float_t, ndim=2] means = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) + cdef np.float_t* current_mean + cdef np.float_t* mean_entry n_means = means.shape[0] # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning ratio = (ratio / float(step)) ** 2 print(ratio) - cdef np.ndarray[dtype=np.float_t, ndim=3] image_yx = np.dstack([grid_y, grid_x, image / ratio]) + cdef np.ndarray[dtype=np.float_t, ndim=3] image_yx = np.dstack([grid_y, grid_x, image / ratio]).copy("C") cdef int i, k, x, y, x_min, x_max, y_min, y_max cdef float dist_mean cdef np.ndarray[dtype=np.int_t, ndim=2] nearest_mean = np.zeros((height, width), dtype=np.int) cdef np.ndarray[dtype=np.float_t, ndim=2] distance = np.ones((height, width), dtype=np.float) * np.inf + cdef np.float_t* image_p = image_yx.data + cdef np.float_t* distance_p = distance.data + cdef np.float_t* current_pixel + cdef float tmp for i in xrange(max_iter): print("iteration %d" % i) nearest_mean_old = nearest_mean.copy() + # we construct a new means every iteration, adjust pointer + current_mean = means.data # assign pixels to means for k in xrange(n_means): # compute windows: - y_min = int(max(means[k, 0] - 2 * step, 0)) - y_max = int(min(means[k, 0] + 2 * step, height)) - x_min = int(max(means[k, 1] - 2 * step, 0)) - x_max = int(min(means[k, 1] + 2 * step, height)) - for x in xrange(x_min, x_max): - for y in xrange(y_min, y_max): + y_min = int(max(current_mean[0] - 2 * step, 0)) + y_max = int(min(current_mean[0] + 2 * step, height)) + x_min = int(max(current_mean[1] - 2 * step, 0)) + x_max = int(min(current_mean[1] + 2 * step, height)) + for y in xrange(y_min, y_max): + current_pixel = &image_p[5 * (y * width + x_min)] + for x in xrange(x_min, x_max): + mean_entry = current_mean dist_mean = 0 for c in range(5): - dist_mean += (image_yx[y, x, c] - means[k, c]) ** 2 + # you would think the compiler can optimize this itself. + # mine can't (with O2) + tmp = current_pixel[0] - mean_entry[0] + current_pixel += 1 + mean_entry += 1 + dist_mean += tmp * tmp if distance[y, x] > dist_mean: nearest_mean[y, x] = k distance[y, x] = dist_mean + current_mean += 5 if (nearest_mean == nearest_mean_old).all(): break # recompute means: means_list = [np.bincount(nearest_mean.ravel(), image_yx[:, :, j].ravel()) for j in xrange(5)] in_mean = np.bincount(nearest_mean.ravel()) - means = (np.vstack(means_list) / in_mean).T + means = (np.vstack(means_list) / in_mean).T.copy("C") return nearest_mean From 1c69adb81783a32c6da234dd748dc48c7d58f063 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Tue, 3 Jul 2012 22:26:52 +0100 Subject: [PATCH 181/648] MISC some simplifications, minor speedup --- skimage/segmentation/km_segmentation.pyx | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/skimage/segmentation/km_segmentation.pyx b/skimage/segmentation/km_segmentation.pyx index 2f1dae5d..f8cfb4e5 100644 --- a/skimage/segmentation/km_segmentation.pyx +++ b/skimage/segmentation/km_segmentation.pyx @@ -24,19 +24,19 @@ def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): ratio = (ratio / float(step)) ** 2 print(ratio) cdef np.ndarray[dtype=np.float_t, ndim=3] image_yx = np.dstack([grid_y, grid_x, image / ratio]).copy("C") - cdef int i, k, x, y, x_min, x_max, y_min, y_max - cdef float dist_mean + cdef int i, k, x, y, x_min, x_max, y_min, y_max, changes + cdef double dist_mean cdef np.ndarray[dtype=np.int_t, ndim=2] nearest_mean = np.zeros((height, width), dtype=np.int) cdef np.ndarray[dtype=np.float_t, ndim=2] distance = np.ones((height, width), dtype=np.float) * np.inf cdef np.float_t* image_p = image_yx.data cdef np.float_t* distance_p = distance.data + cdef np.float_t* current_distance cdef np.float_t* current_pixel - cdef float tmp + cdef double tmp for i in xrange(max_iter): + changes = 0 print("iteration %d" % i) - nearest_mean_old = nearest_mean.copy() - # we construct a new means every iteration, adjust pointer current_mean = means.data # assign pixels to means for k in xrange(n_means): @@ -47,6 +47,7 @@ def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): x_max = int(min(current_mean[1] + 2 * step, height)) for y in xrange(y_min, y_max): current_pixel = &image_p[5 * (y * width + x_min)] + current_distance = &distance_p[y * width + x_min] for x in xrange(x_min, x_max): mean_entry = current_mean dist_mean = 0 @@ -54,14 +55,16 @@ def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): # you would think the compiler can optimize this itself. # mine can't (with O2) tmp = current_pixel[0] - mean_entry[0] + dist_mean += tmp * tmp current_pixel += 1 mean_entry += 1 - dist_mean += tmp * tmp - if distance[y, x] > dist_mean: + # some precision issue here. Doesnt work if testing ">" + if current_distance[0] - dist_mean > 1e-10: nearest_mean[y, x] = k - distance[y, x] = dist_mean + current_distance[0] = dist_mean + changes += 1 + current_distance += 1 current_mean += 5 - if (nearest_mean == nearest_mean_old).all(): break # recompute means: means_list = [np.bincount(nearest_mean.ravel(), image_yx[:, :, j].ravel()) From d9a22d867bacca4ea297cff286813359edac110c Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Wed, 18 Jul 2012 07:25:39 +0100 Subject: [PATCH 182/648] Documentation, example for km_segmentation --- doc/examples/plot_km_segmentation.py | 36 ++++++++++++++++++++++ skimage/segmentation/km_segmentation.pyx | 39 ++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 doc/examples/plot_km_segmentation.py diff --git a/doc/examples/plot_km_segmentation.py b/doc/examples/plot_km_segmentation.py new file mode 100644 index 00000000..ed8cf983 --- /dev/null +++ b/doc/examples/plot_km_segmentation.py @@ -0,0 +1,36 @@ +""" +""" +print __doc__ + +import matplotlib.pyplot as plt +import numpy as np + +from skimage.data import lena +from skimage.segmentation import km_segmentation +from skimage.util import img_as_float + +img = img_as_float(lena()).copy("C") +segments = km_segmentation(img, ratio=2.0, n_segments=200) + +print("number of segments: %d" % len(np.unique(segments))) + +plt.subplot(131, title="original") +plt.imshow(img, interpolation='nearest') +plt.axis("off") + +plt.subplot(132, title="superpixels") +# shuffle the labels for better visualization +plt.imshow(segments, interpolation='nearest', cmap=plt.cm.prism) +plt.axis("off") + +plt.subplot(133, title="mean color") +colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in + xrange(img.shape[2])] +counts = np.bincount(segments.ravel()) +colors = np.vstack(colors) / counts +plt.imshow(colors.T[segments], interpolation='nearest') +plt.axis("off") + +plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, + bottom=0.02, left=0.02, right=0.98) +plt.show() diff --git a/skimage/segmentation/km_segmentation.pyx b/skimage/segmentation/km_segmentation.pyx index f8cfb4e5..d0674be4 100644 --- a/skimage/segmentation/km_segmentation.pyx +++ b/skimage/segmentation/km_segmentation.pyx @@ -1,11 +1,47 @@ import numpy as np cimport numpy as np +from time import time from scipy import ndimage from ..util import img_as_float def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): + """Segments image using k-means clustering in Color-(x,y) space. + + Parameters + ---------- + image: (width, height, 3) ndarray + Input image + ratio: float + Balances color-space proximity and image-space proximity. + Higher values give more weight to color-space. + max_iter: int + maximum number of iterations of k-means + sigma: float + Width of Gaussian smoothing kernel for preprocessing. + + Returns + ------- + segment_mask: ndarray, [width, height] + Integer mask indicating segment labels. + + Notes + ----- + The image is smoothed using a Gaussian kernel prior to segmentation. + Best results are achieved if the image is given in Lab color space. + + References + ---------- + .. [1] Slic superpixels, Achanta, R. and Shaji, A. and Smith, K. and Lucchi, + A. and Fua, P. and Suesstrunk, S. + Technical Report 2010 + + """ + image = np.atleast_3d(image) + if image.shape[2] != 3: + ValueError("Only 3-channel 2d images are supported.") image = ndimage.gaussian_filter(img_as_float(image), sigma) + # initialize on grid: height, width = image.shape[:2] # approximate grid size for desired n_segments @@ -22,7 +58,6 @@ def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning ratio = (ratio / float(step)) ** 2 - print(ratio) cdef np.ndarray[dtype=np.float_t, ndim=3] image_yx = np.dstack([grid_y, grid_x, image / ratio]).copy("C") cdef int i, k, x, y, x_min, x_max, y_min, y_max, changes cdef double dist_mean @@ -36,7 +71,6 @@ def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): cdef double tmp for i in xrange(max_iter): changes = 0 - print("iteration %d" % i) current_mean = means.data # assign pixels to means for k in xrange(n_means): @@ -65,6 +99,7 @@ def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): changes += 1 current_distance += 1 current_mean += 5 + if changes == 0: break # recompute means: means_list = [np.bincount(nearest_mean.ravel(), image_yx[:, :, j].ravel()) From 026b6b1df06f0af452b5ed33b3a6d3cfffbd943f Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 3 Aug 2012 11:43:03 +0100 Subject: [PATCH 183/648] Fix initialization in km_segmentation, prettier examples --- doc/examples/plot_km_segmentation.py | 25 ++++-------------- doc/examples/plot_quickshift.py | 31 ++++++++--------------- skimage/__init__.py | 1 + skimage/filter/tests/test_thresholding.py | 2 ++ skimage/segmentation/__init__.py | 3 ++- skimage/segmentation/boundaries.py | 19 ++++++++++++++ skimage/segmentation/km_segmentation.pyx | 11 +++++--- 7 files changed, 46 insertions(+), 46 deletions(-) create mode 100644 skimage/segmentation/boundaries.py diff --git a/doc/examples/plot_km_segmentation.py b/doc/examples/plot_km_segmentation.py index ed8cf983..3a5d6128 100644 --- a/doc/examples/plot_km_segmentation.py +++ b/doc/examples/plot_km_segmentation.py @@ -6,31 +6,16 @@ import matplotlib.pyplot as plt import numpy as np from skimage.data import lena -from skimage.segmentation import km_segmentation +from skimage.segmentation import km_segmentation, visualize_boundaries from skimage.util import img_as_float +from skimage.color import rgb2lab img = img_as_float(lena()).copy("C") -segments = km_segmentation(img, ratio=2.0, n_segments=200) +segments = km_segmentation(rgb2lab(img), ratio=10.0, n_segments=1000) print("number of segments: %d" % len(np.unique(segments))) -plt.subplot(131, title="original") -plt.imshow(img, interpolation='nearest') +boundaries_mine = visualize_boundaries(img, segments) +plt.imshow(boundaries_mine) plt.axis("off") - -plt.subplot(132, title="superpixels") -# shuffle the labels for better visualization -plt.imshow(segments, interpolation='nearest', cmap=plt.cm.prism) -plt.axis("off") - -plt.subplot(133, title="mean color") -colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in - xrange(img.shape[2])] -counts = np.bincount(segments.ravel()) -colors = np.vstack(colors) / counts -plt.imshow(colors.T[segments], interpolation='nearest') -plt.axis("off") - -plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, - bottom=0.02, left=0.02, right=0.98) plt.show() diff --git a/doc/examples/plot_quickshift.py b/doc/examples/plot_quickshift.py index 5adf1969..9b096459 100644 --- a/doc/examples/plot_quickshift.py +++ b/doc/examples/plot_quickshift.py @@ -25,30 +25,19 @@ import matplotlib.pyplot as plt import numpy as np from skimage.data import lena -from skimage.segmentation import quickshift +from skimage.segmentation import quickshift, visualize_boundaries from skimage.util import img_as_float +from skimage.color import rgb2lab img = img_as_float(lena())[::2, ::2, :].copy("C") -segments = quickshift(img, kernel_size=5, max_dist=20) +segments = quickshift(rgb2lab(img), kernel_size=5, max_dist=20) +segments_rgb = quickshift(img, kernel_size=5, max_dist=20) print("number of segments: %d" % len(np.unique(segments))) - -fig, (ax_org, ax_sp, ax_mean) = plt.subplots(1, 3) -ax_org.set_title("original") -ax_org.imshow(img, interpolation='nearest') -ax_org.axis("off") - -ax_sp.set_title("superpixels") -ax_sp.imshow(segments, interpolation='nearest', cmap=plt.cm.prism) -ax_sp.axis("off") - -colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in - xrange(img.shape[2])] -counts = np.bincount(segments.ravel()) -colors = np.vstack(colors) / counts -ax_mean.set_title("mean color") -ax_mean.imshow(colors.T[segments], interpolation='nearest') -ax_mean.axis("off") -fig.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, - bottom=0.02, left=0.02, right=0.98) +boundaries = visualize_boundaries(img, segments) +boundaries_rgb = visualize_boundaries(img, segments_rgb) +plt.imshow(boundaries) +plt.figure() +plt.imshow(boundaries_rgb) +plt.axis("off") plt.show() diff --git a/skimage/__init__.py b/skimage/__init__.py index ad37f425..b1c331ff 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -88,6 +88,7 @@ test = _setup_test() test_verbose = _setup_test(verbose=True) + def get_log(name=None): """Return a console logger. diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 97d3d9e3..d17f9b84 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -77,11 +77,13 @@ def test_otsu_camera_image(): assert 86 < threshold_otsu(camera) < 88 + def test_otsu_coins_image(): coins = skimage.img_as_ubyte(data.coins()) assert 106 < threshold_otsu(coins) < 108 + def test_otsu_coins_image_as_float(): coins = skimage.img_as_float(data.coins()) assert 0.41 < threshold_otsu(coins) < 0.42 diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index b2c97448..380e937d 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -2,6 +2,7 @@ from .random_walker_segmentation import random_walker from .felzenszwalb import felzenszwalb_segmentation from .km_segmentation import km_segmentation from .quickshift import quickshift +from .boundaries import find_boundaries, visualize_boundaries __all__ = [random_walker, quickshift, felzenszwalb_segmentation, - km_segmentation] + km_segmentation, find_boundaries, visualize_boundaries] diff --git a/skimage/segmentation/boundaries.py b/skimage/segmentation/boundaries.py new file mode 100644 index 00000000..b40ecb01 --- /dev/null +++ b/skimage/segmentation/boundaries.py @@ -0,0 +1,19 @@ +import numpy as np +from ..morphology import dilation, square +from ..util import img_as_float + + +def find_boundaries(label_img): + boundaries = np.zeros(label_img.shape, dtype=np.bool) + boundaries[1:, :] += label_img[1:, :] != label_img[:-1, :] + boundaries[:, 1:] += label_img[:, 1:] != label_img[:, :-1] + return boundaries + + +def visualize_boundaries(img, label_img): + img = img_as_float(img, force_copy=True) + boundaries = find_boundaries(label_img) + outer_boundaries = dilation(boundaries.astype(np.uint8), square(2)) + img[outer_boundaries != 0, :] = np.array([0, 0, 0]) # black + img[boundaries, :] = np.array([1, 1, 0]) # yellow + return img diff --git a/skimage/segmentation/km_segmentation.pyx b/skimage/segmentation/km_segmentation.pyx index d0674be4..47faab7f 100644 --- a/skimage/segmentation/km_segmentation.pyx +++ b/skimage/segmentation/km_segmentation.pyx @@ -5,12 +5,12 @@ from scipy import ndimage from ..util import img_as_float -def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): +def km_segmentation(image, n_segments=100, ratio=10., max_iter=10, sigma=1): """Segments image using k-means clustering in Color-(x,y) space. Parameters ---------- - image: (width, height, 3) ndarray + image: (width, height, 3) ndarray Input image ratio: float Balances color-space proximity and image-space proximity. @@ -50,7 +50,8 @@ def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): means_y = grid_y[::step, ::step] means_x = grid_x[::step, ::step] - means_color = image[means_y, means_x, :] + n_seeds = len(means_y) + means_color = np.zeros((n_seeds, n_seeds, 3)) cdef np.ndarray[dtype=np.float_t, ndim=2] means = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) cdef np.float_t* current_mean cdef np.float_t* mean_entry @@ -63,13 +64,14 @@ def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): cdef double dist_mean cdef np.ndarray[dtype=np.int_t, ndim=2] nearest_mean = np.zeros((height, width), dtype=np.int) - cdef np.ndarray[dtype=np.float_t, ndim=2] distance = np.ones((height, width), dtype=np.float) * np.inf + cdef np.ndarray[dtype=np.float_t, ndim=2] distance = np.empty((height, width)) cdef np.float_t* image_p = image_yx.data cdef np.float_t* distance_p = distance.data cdef np.float_t* current_distance cdef np.float_t* current_pixel cdef double tmp for i in xrange(max_iter): + distance.fill(np.inf) changes = 0 current_mean = means.data # assign pixels to means @@ -105,5 +107,6 @@ def km_segmentation(image, n_segments=100, ratio=10., max_iter=100, sigma=1.0): means_list = [np.bincount(nearest_mean.ravel(), image_yx[:, :, j].ravel()) for j in xrange(5)] in_mean = np.bincount(nearest_mean.ravel()) + in_mean[in_mean == 0] = 1 means = (np.vstack(means_list) / in_mean).T.copy("C") return nearest_mean From cd1007a0bc3ad88def99ad58acb709e72c341490 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 3 Aug 2012 11:57:02 +0100 Subject: [PATCH 184/648] Rename km_segmentation to slic. They have a PAMI paper now so I guess we should use their name. --- doc/examples/{plot_km_segmentation.py => plot_slic.py} | 4 ++-- skimage/segmentation/__init__.py | 4 ++-- skimage/segmentation/setup.py | 4 ++-- skimage/segmentation/{km_segmentation.pyx => slic.pyx} | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) rename doc/examples/{plot_km_segmentation.py => plot_slic.py} (73%) rename skimage/segmentation/{km_segmentation.pyx => slic.pyx} (93%) diff --git a/doc/examples/plot_km_segmentation.py b/doc/examples/plot_slic.py similarity index 73% rename from doc/examples/plot_km_segmentation.py rename to doc/examples/plot_slic.py index 3a5d6128..aa743276 100644 --- a/doc/examples/plot_km_segmentation.py +++ b/doc/examples/plot_slic.py @@ -6,12 +6,12 @@ import matplotlib.pyplot as plt import numpy as np from skimage.data import lena -from skimage.segmentation import km_segmentation, visualize_boundaries +from skimage.segmentation import slic, visualize_boundaries from skimage.util import img_as_float from skimage.color import rgb2lab img = img_as_float(lena()).copy("C") -segments = km_segmentation(rgb2lab(img), ratio=10.0, n_segments=1000) +segments = slic(rgb2lab(img), ratio=10.0, n_segments=1000) print("number of segments: %d" % len(np.unique(segments))) diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 380e937d..2fb6902c 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1,8 +1,8 @@ from .random_walker_segmentation import random_walker from .felzenszwalb import felzenszwalb_segmentation -from .km_segmentation import km_segmentation +from .slic import slic from .quickshift import quickshift from .boundaries import find_boundaries, visualize_boundaries __all__ = [random_walker, quickshift, felzenszwalb_segmentation, - km_segmentation, find_boundaries, visualize_boundaries] + slic, find_boundaries, visualize_boundaries] diff --git a/skimage/segmentation/setup.py b/skimage/segmentation/setup.py index 0be6b748..7b7d9cf1 100644 --- a/skimage/segmentation/setup.py +++ b/skimage/segmentation/setup.py @@ -17,8 +17,8 @@ def configuration(parent_package='', top_path=None): cython(['quickshift.pyx'], working_path=base_path) config.add_extension('quickshift', sources=['quickshift.c'], include_dirs=[get_numpy_include_dirs()]) - cython(['km_segmentation.pyx'], working_path=base_path) - config.add_extension('km_segmentation', sources=['km_segmentation.c'], + cython(['slic.pyx'], working_path=base_path) + config.add_extension('slic', sources=['slic.c'], include_dirs=[get_numpy_include_dirs()]) return config diff --git a/skimage/segmentation/km_segmentation.pyx b/skimage/segmentation/slic.pyx similarity index 93% rename from skimage/segmentation/km_segmentation.pyx rename to skimage/segmentation/slic.pyx index 47faab7f..3fcd094a 100644 --- a/skimage/segmentation/km_segmentation.pyx +++ b/skimage/segmentation/slic.pyx @@ -5,7 +5,7 @@ from scipy import ndimage from ..util import img_as_float -def km_segmentation(image, n_segments=100, ratio=10., max_iter=10, sigma=1): +def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1): """Segments image using k-means clustering in Color-(x,y) space. Parameters @@ -32,9 +32,9 @@ def km_segmentation(image, n_segments=100, ratio=10., max_iter=10, sigma=1): References ---------- - .. [1] Slic superpixels, Achanta, R. and Shaji, A. and Smith, K. and Lucchi, - A. and Fua, P. and Suesstrunk, S. - Technical Report 2010 + .. [1] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, + Pascal Fua, and Sabine Süsstrunk, SLIC Superpixels Compared to + State-of-the-art Superpixel Methods, TPAMI, May 2012. """ image = np.atleast_3d(image) From b6059b5672ccff80c9e70d58aa010ef4ac55a076 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 3 Aug 2012 12:00:39 +0100 Subject: [PATCH 185/648] Put RGB2Lab into slic as it seems to be essential. --- doc/examples/plot_slic.py | 3 +-- skimage/segmentation/slic.pyx | 9 +++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/examples/plot_slic.py b/doc/examples/plot_slic.py index aa743276..8ff59d98 100644 --- a/doc/examples/plot_slic.py +++ b/doc/examples/plot_slic.py @@ -8,10 +8,9 @@ import numpy as np from skimage.data import lena from skimage.segmentation import slic, visualize_boundaries from skimage.util import img_as_float -from skimage.color import rgb2lab img = img_as_float(lena()).copy("C") -segments = slic(rgb2lab(img), ratio=10.0, n_segments=1000) +segments = slic(img, ratio=10.0, n_segments=1000) print("number of segments: %d" % len(np.unique(segments))) diff --git a/skimage/segmentation/slic.pyx b/skimage/segmentation/slic.pyx index 3fcd094a..1430ba63 100644 --- a/skimage/segmentation/slic.pyx +++ b/skimage/segmentation/slic.pyx @@ -3,9 +3,10 @@ cimport numpy as np from time import time from scipy import ndimage from ..util import img_as_float +from ..color import rgb2lab -def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1): +def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, convert2lab=True): """Segments image using k-means clustering in Color-(x,y) space. Parameters @@ -19,6 +20,9 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1): maximum number of iterations of k-means sigma: float Width of Gaussian smoothing kernel for preprocessing. + convert2lab: bool + Whether the input should be converted to Lab colorspace prior to segmentation. + For this purpose, the input is assumed to be RGB. Highly recommended. Returns ------- @@ -28,7 +32,6 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1): Notes ----- The image is smoothed using a Gaussian kernel prior to segmentation. - Best results are achieved if the image is given in Lab color space. References ---------- @@ -41,6 +44,8 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1): if image.shape[2] != 3: ValueError("Only 3-channel 2d images are supported.") image = ndimage.gaussian_filter(img_as_float(image), sigma) + if convert2lab: + image = rgb2lab(image) # initialize on grid: height, width = image.shape[:2] From 54af4176dd3de0adc2cdd1e819910e4897831598 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 3 Aug 2012 20:45:01 -0400 Subject: [PATCH 186/648] ENH: Add RequiredAttr to raise warnings when attr not set. --- skimage/viewer/plugins/base.py | 16 ++++------------ skimage/viewer/utils/core.py | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index ee2cbb5d..562237b2 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -3,9 +3,10 @@ Base class for Plugins that interact with ImageViewer. """ from PyQt4 import QtGui from PyQt4.QtCore import Qt - import matplotlib as mpl +from ..utils import RequiredAttr + class Plugin(QtGui.QDialog): """Base class for plugins that interact with an ImageViewer. @@ -68,6 +69,7 @@ class Plugin(QtGui.QDialog): """ name = 'Plugin' + image_viewer = RequiredAttr("%s is not attached to ImageViewer" % name) draws_on_image = False def __init__(self, image_filter=None, height=0, width=400, useblit=None): @@ -92,18 +94,8 @@ class Plugin(QtGui.QDialog): self.cids = [] self.artists = [] - @property - def image_viewer(self): - if self._image_viewer is None: - raise RuntimeError("Plugin is not attached to ImageViewer") - return self._image_viewer - - @image_viewer.setter - def image_viewer(self, image_viewer): - self._image_viewer = image_viewer - def attach(self, image_viewer): - """Attach the plugin to an ImageViewer. + """Attach the plugin to an ImageViewer. Note that the ImageViewer will automatically call this method when the plugin is added to the ImageViewer. For example: diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 81d66183..36476df8 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -5,7 +5,24 @@ from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg from PyQt4 import QtGui -__all__ = ['figimage', 'LinearColormap', 'ClearColormap', 'MatplotlibCanvas'] +__all__ = ['figimage', 'LinearColormap', 'ClearColormap', 'MatplotlibCanvas', + 'RequiredAttr'] + + +class RequiredAttr(object): + """A class attribute that must be set before use.""" + + def __init__(self, msg): + self.msg = msg + self.val = None + + def __get__(self, obj, objtype): + if self.val is None: + raise RuntimeError(self.msg) + return self.val + + def __set__(self, obj, val): + self.val = val def figimage(image, scale=1, dpi=None, **kwargs): From 9ac42728c6d542570d7cca28d848b660fe0c020c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 3 Aug 2012 20:47:42 -0400 Subject: [PATCH 187/648] DOC: Clean up docstring for Slider --- skimage/viewer/widgets/core.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 7e1b6e9e..27cb187f 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -11,8 +11,8 @@ parameter type specified by its `ptype` attribute, which can be: 'arg' : positional argument passed to Plugin's `filter_image` method. 'kwarg' : keyword argument passed to Plugin's `filter_image` method. - 'plugin' : attribute of Plugin. You'll probably need to make the attribute - a class property that updates the display. + 'plugin' : attribute of Plugin. You'll probably need to add a class + property of the same name that updates the display. """ from PyQt4.QtCore import Qt @@ -41,16 +41,7 @@ class BaseWidget(QtGui.QWidget): class Slider(BaseWidget): - """Slider widget. - - 'name' attribute and calls a callback - with 'name' as an argument to the registered callback. - - This allows you to create large groups of sliders in a loop, - but still keep track of the individual events - - It also prints a label below the slider. - + """Slider widget for adjusting numeric parameters. Parameters ---------- From 4ab583ba31800fa986a63e00c1ecc475889b2dea Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 3 Aug 2012 21:50:28 -0400 Subject: [PATCH 188/648] ENH: Add SaveButtons widget. --- skimage/viewer/plugins/base.py | 1 + skimage/viewer/widgets/__init__.py | 1 + skimage/viewer/widgets/core.py | 5 ++ skimage/viewer/widgets/history.py | 66 ++++++++++++++++++++++++ viewer_examples/plugins/median_filter.py | 18 +++++++ 5 files changed, 91 insertions(+) create mode 100644 skimage/viewer/widgets/history.py create mode 100644 viewer_examples/plugins/median_filter.py diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 562237b2..7c00ded8 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -137,6 +137,7 @@ class Plugin(QtGui.QDialog): widget.callback = self.filter_image elif widget.ptype == 'plugin': widget.callback = self.update_plugin + widget.plugin = self self.layout.addWidget(widget, self.row, 0) self.row += 1 diff --git a/skimage/viewer/widgets/__init__.py b/skimage/viewer/widgets/__init__.py index 5af24064..6552a313 100644 --- a/skimage/viewer/widgets/__init__.py +++ b/skimage/viewer/widgets/__init__.py @@ -1 +1,2 @@ from core import * +from history import * diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 27cb187f..8610f910 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -19,17 +19,22 @@ from PyQt4.QtCore import Qt from PyQt4 import QtGui from PyQt4 import QtCore +from ..utils import RequiredAttr + __all__ = ['BaseWidget', 'Slider', 'ComboBox'] class BaseWidget(QtGui.QWidget): + plugin = RequiredAttr("Widget is not attached to a Plugin.") + def __init__(self, name, ptype, callback): super(BaseWidget, self).__init__() self.name = name self.ptype = ptype self.callback = callback + self.plugin = None @property def val(self): diff --git a/skimage/viewer/widgets/history.py b/skimage/viewer/widgets/history.py new file mode 100644 index 00000000..daaf38cd --- /dev/null +++ b/skimage/viewer/widgets/history.py @@ -0,0 +1,66 @@ +import os +from textwrap import dedent + +from PyQt4 import QtGui + +from skimage import io +from .core import BaseWidget + + +__all__ = ['SaveButtons'] + + +class SaveButtons(BaseWidget): + + def __init__(self, default_format='png'): + name = 'Save to:' + ptype = None + callback = None + super(SaveButtons, self).__init__(name, ptype, callback) + + self.default_format = default_format + + self.name_label = QtGui.QLabel() + self.name_label.setText(name) + + self.save_file = QtGui.QPushButton('File') + self.save_file.clicked.connect(self.save_to_file) + self.save_stack = QtGui.QPushButton('Stack') + self.save_stack.clicked.connect(self.save_to_stack) + + self.layout = QtGui.QHBoxLayout(self) + self.layout.addWidget(self.name_label) + self.layout.addWidget(self.save_stack) + self.layout.addWidget(self.save_file) + + def save_to_stack(self): + image = self.plugin.image_viewer.image.copy() + io.push(image) + + msg = dedent('''\ + The image has been pushed to the io stack. + Use io.pop() to retrieve the most recently pushed image. + NOTE: The io stack only works in interactive sessions.''') + notify(msg) + + def save_to_file(self): + filename = str(QtGui.QFileDialog.getSaveFileName()) + if len(filename) == 0: + return + #TODO: io plugins should assign default image formats + basename, ext = os.path.splitext(filename) + if not ext: + filename = '%s.%s' % (filename, self.default_format) + io.imsave(filename, self.plugin.image_viewer.image) + + +def notify(msg): + msglabel = QtGui.QLabel(msg) + dialog = QtGui.QDialog() + ok = QtGui.QPushButton('OK', dialog) + ok.clicked.connect(dialog.accept) + ok.setDefault(True) + dialog.layout = QtGui.QGridLayout(dialog) + dialog.layout.addWidget(msglabel, 0, 0, 1, 3) + dialog.layout.addWidget(ok, 1, 1) + dialog.exec_() diff --git a/viewer_examples/plugins/median_filter.py b/viewer_examples/plugins/median_filter.py new file mode 100644 index 00000000..57d04397 --- /dev/null +++ b/viewer_examples/plugins/median_filter.py @@ -0,0 +1,18 @@ +from skimage import data +from skimage.filter import median_filter + +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import Slider +from skimage.viewer.widgets.history import SaveButtons +from skimage.viewer.plugins.overlayplugin import Plugin + + +image = data.coins() +viewer = ImageViewer(image) + +plugin = Plugin(image_filter=median_filter) +plugin += Slider('radius', 2, 10, value_type='int', update_on='release') +plugin += SaveButtons() + +viewer += plugin +viewer.show() From 398b320477186c4b89d00d099e6fe587085abbc0 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 3 Aug 2012 22:21:59 -0400 Subject: [PATCH 189/648] BUG: reset image when plugin is closed. --- skimage/viewer/plugins/base.py | 1 + skimage/viewer/viewers/core.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 7c00ded8..fc2951b2 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -200,6 +200,7 @@ class Plugin(QtGui.QDialog): self.disconnect_image_events() self.remove_image_artists() self.image_viewer.plugins.remove(self) + self.image_viewer.reset_image() self.image_viewer.redraw() self.close() diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index dc38b422..3775e511 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -76,7 +76,7 @@ class ImageViewer(QtGui.QMainWindow): self._image_plot = self.ax.images[0] self.original_image = image - self.image = image + self.image = image.copy() self.plugins = [] # List of axes artists to check for removal. @@ -141,6 +141,9 @@ class ImageViewer(QtGui.QMainWindow): self._image_plot.set_array(image) self.redraw() + def reset_image(self): + self.image = self.original_image.copy() + def connect_event(self, event, callback): """Connect callback function to matplotlib event and return id.""" cid = self.canvas.mpl_connect(event, callback) From e96aca563792d1bab41ccdd239b452be362e2fff Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 3 Aug 2012 22:27:05 -0400 Subject: [PATCH 190/648] ENH: Add OK/Cancel buttons --- skimage/viewer/widgets/core.py | 2 +- skimage/viewer/widgets/history.py | 39 ++++++++++++++++++++---- viewer_examples/plugins/median_filter.py | 5 +-- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 8610f910..625e707b 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -29,7 +29,7 @@ class BaseWidget(QtGui.QWidget): plugin = RequiredAttr("Widget is not attached to a Plugin.") - def __init__(self, name, ptype, callback): + def __init__(self, name, ptype=None, callback=None): super(BaseWidget, self).__init__() self.name = name self.ptype = ptype diff --git a/skimage/viewer/widgets/history.py b/skimage/viewer/widgets/history.py index daaf38cd..cc586579 100644 --- a/skimage/viewer/widgets/history.py +++ b/skimage/viewer/widgets/history.py @@ -7,16 +7,43 @@ from skimage import io from .core import BaseWidget -__all__ = ['SaveButtons'] +__all__ = ['OKCancelButtons', 'SaveButtons'] + + +class OKCancelButtons(BaseWidget): + """Buttons that close the parent plugin. + + OK will replace the original image with the current (filtered) image. + Cancel will just close the plugin. + """ + def __init__(self): + name = 'OK/Cancel' + super(OKCancelButtons, self).__init__(name) + + self.ok = QtGui.QPushButton('OK') + self.ok.clicked.connect(self.update_original_image) + self.cancel = QtGui.QPushButton('Cancel') + self.cancel.clicked.connect(self.close_plugin) + + self.layout = QtGui.QHBoxLayout(self) + self.layout.addWidget(self.cancel) + self.layout.addWidget(self.ok) + + def update_original_image(self): + image = self.plugin.image_viewer.image + self.plugin.image_viewer.original_image = image + self.plugin.close() + + def close_plugin(self): + # Image viewer will restore original image on close. + self.plugin.close() class SaveButtons(BaseWidget): + """Buttons to save image to io.stack or to a file.""" - def __init__(self, default_format='png'): - name = 'Save to:' - ptype = None - callback = None - super(SaveButtons, self).__init__(name, ptype, callback) + def __init__(self, name='Save to:', default_format='png'): + super(SaveButtons, self).__init__(name) self.default_format = default_format diff --git a/viewer_examples/plugins/median_filter.py b/viewer_examples/plugins/median_filter.py index 57d04397..3a050382 100644 --- a/viewer_examples/plugins/median_filter.py +++ b/viewer_examples/plugins/median_filter.py @@ -3,8 +3,8 @@ from skimage.filter import median_filter from skimage.viewer import ImageViewer from skimage.viewer.widgets import Slider -from skimage.viewer.widgets.history import SaveButtons -from skimage.viewer.plugins.overlayplugin import Plugin +from skimage.viewer.widgets.history import OKCancelButtons, SaveButtons +from skimage.viewer.plugins.base import Plugin image = data.coins() @@ -13,6 +13,7 @@ viewer = ImageViewer(image) plugin = Plugin(image_filter=median_filter) plugin += Slider('radius', 2, 10, value_type='int', update_on='release') plugin += SaveButtons() +plugin += OKCancelButtons() viewer += plugin viewer.show() From cfd0b84a9b0a99a7ba39913baa6f1ddbdc0d7a3f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 3 Aug 2012 23:04:51 -0400 Subject: [PATCH 191/648] STY: Tweak button sizes. --- skimage/viewer/widgets/history.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/widgets/history.py b/skimage/viewer/widgets/history.py index cc586579..bb65b7dd 100644 --- a/skimage/viewer/widgets/history.py +++ b/skimage/viewer/widgets/history.py @@ -16,16 +16,19 @@ class OKCancelButtons(BaseWidget): OK will replace the original image with the current (filtered) image. Cancel will just close the plugin. """ - def __init__(self): + def __init__(self, button_width=80): name = 'OK/Cancel' super(OKCancelButtons, self).__init__(name) self.ok = QtGui.QPushButton('OK') self.ok.clicked.connect(self.update_original_image) + self.ok.setMaximumWidth(button_width) self.cancel = QtGui.QPushButton('Cancel') self.cancel.clicked.connect(self.close_plugin) + self.cancel.setMaximumWidth(button_width) self.layout = QtGui.QHBoxLayout(self) + self.layout.addStretch() self.layout.addWidget(self.cancel) self.layout.addWidget(self.ok) From 8e28e39887a2a237c7cf5c3111eb5a6354bd1391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 4 Aug 2012 10:54:15 +0200 Subject: [PATCH 192/648] add test case for correct quadrant determination --- skimage/measure/tests/test_regionprops.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index 4408c3d1..915087dc 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -1,6 +1,7 @@ from numpy.testing import assert_array_equal, assert_almost_equal, \ assert_array_almost_equal import numpy as np +import math from skimage.measure import regionprops @@ -193,6 +194,9 @@ def test_orientation(): orientation = regionprops(SAMPLE, ['Orientation'])[0]['Orientation'] # determined with MATLAB assert_almost_equal(orientation, 0.10446844651921) + # test correct quadrant determination + orientation2 = regionprops(SAMPLE.T, ['Orientation'])[0]['Orientation'] + assert_almost_equal(orientation2, math.pi / 2 - orientation) def test_perimeter(): perimeter = regionprops(SAMPLE, ['Perimeter'])[0]['Perimeter'] From 1746d2c38c0c059c4d87879a3bcc7d92bc65ed98 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 4 Aug 2012 18:03:03 +0100 Subject: [PATCH 193/648] Some scaling issues to be closer to reference implementation --- skimage/segmentation/quickshift.pyx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index b0a9b2d6..e37463b3 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -65,7 +65,6 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, r # we get crazy memory overhead (width * height * windowsize**2) # TODO do smoothing beforehand? - # TODO manage borders somehow? # TODO join orphant roots? # window size for neighboring pixels to consider @@ -92,7 +91,7 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, r for c in xrange(channels): dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 dist += (x - x_)**2 + (y - y_)**2 - densities[x, y] += exp(-dist / kernel_size) + densities[x, y] += exp(-dist / (2 * kernel_size**2)) current_pixel_p += channels # this will break ties that otherwise would give us headache @@ -117,7 +116,7 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, r if dist < closest: closest = dist parent[x, y] = x_ * width + y_ - dist_parent[x, y] = closest + dist_parent[x, y] = np.sqrt(closest) current_pixel_p += channels dist_parent_flat = dist_parent.ravel() @@ -130,5 +129,5 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, r flat = np.unique(flat, return_inverse=True)[1] flat = flat.reshape(width, height) if return_tree: - return flat, parent + return flat, parent, dist_parent return flat From d770fc714d0bb43ac47a7c3e36b02c9791210efe Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 4 Aug 2012 19:56:22 +0100 Subject: [PATCH 194/648] quickshift: convert to lab in function, some comments in code. --- skimage/segmentation/quickshift.pyx | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index e37463b3..3fcc94f7 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -5,6 +5,7 @@ cimport cython from itertools import product from ..util import img_as_float +from ..color import rgb2lab cdef extern from "math.h": @@ -14,7 +15,7 @@ cdef extern from "math.h": @cython.boundscheck(False) @cython.wraparound(False) @cython.cdivision(True) -def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, random_seed=None): +def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, convert2lab=True, random_seed=None): """Segments image using quickshift clustering in Color-(x,y) space. Produces an oversegmentation of the image using the quickshift mode-seeking algorithm. @@ -34,6 +35,9 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, r Higher means less clusters. return_tree: bool Whether to return the full segmentation hierarchy tree + convert2lab: bool + Whether the input should be converted to Lab colorspace prior to segmentation. + For this purpose, the input is assumed to be RGB. random_seed: None or int Random seed used for breaking ties @@ -44,7 +48,8 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, r Notes ----- - The authors advocate to convert the image to Lab color space prior to segmentation. + The authors advocate to convert the image to Lab color space prior to segmentation, though + this is not strictly necessary. For this to work, the image must be given in RGB format. References ---------- @@ -53,8 +58,13 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, r """ - image = np.atleast_3d(image) - cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c = np.ascontiguousarray(img_as_float(image)) * ratio + image = img_as_float(np.atleast_3d(image)) + if convert2lab: + if image.shape[2] != 3: + ValueError("Only RGB images can be converted to Lab space.") + image = rgb2lab(image) + + cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c = np.ascontiguousarray(image) * ratio if random_seed is None: random_state = np.random.RandomState() @@ -64,13 +74,16 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, r # We compute the distances twice since otherwise # we get crazy memory overhead (width * height * windowsize**2) - # TODO do smoothing beforehand? # TODO join orphant roots? + # Some nodes might not have a point of higher density within the + # search window. We could do a global search over these in the end. + # Reference implementation doesn't do that, though, and it only has + # an effect for very high max_dist. # window size for neighboring pixels to consider if kernel_size < 1: raise ValueError("Sigma should be >= 1") - cdef int w = int(2 * kernel_size) + cdef int w = int(3 * kernel_size) cdef int width = image_c.shape[0] cdef int height = image_c.shape[1] @@ -95,8 +108,8 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, r current_pixel_p += channels # this will break ties that otherwise would give us headache - densities += random_state.normal(scale=0.00001, size=(width, height)) + # default parent to self: cdef np.ndarray[dtype=np.int_t, ndim=2] parent = np.arange(width * height).reshape(width, height) cdef np.ndarray[dtype=np.float_t, ndim=2] dist_parent = np.zeros((width, height)) @@ -121,8 +134,10 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, r dist_parent_flat = dist_parent.ravel() flat = parent.ravel() + # remove parents with distance > max_dist flat[dist_parent_flat > max_dist] = np.arange(width * height)[dist_parent_flat > max_dist] old = np.zeros_like(flat) + # flatten forest (mark each pixel with root of corresponding tree) while (old != flat).any(): old = flat flat = flat[flat] From 10934cfaff34e427481871c2ffa43939cb714896 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 4 Aug 2012 20:27:18 +0100 Subject: [PATCH 195/648] Fixed bug in SLIC smoothing --- skimage/segmentation/slic.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/slic.pyx b/skimage/segmentation/slic.pyx index 1430ba63..37c6b79c 100644 --- a/skimage/segmentation/slic.pyx +++ b/skimage/segmentation/slic.pyx @@ -19,7 +19,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, convert2lab=Tru max_iter: int maximum number of iterations of k-means sigma: float - Width of Gaussian smoothing kernel for preprocessing. + Width of Gaussian smoothing kernel for preprocessing. Zero means no smoothing. convert2lab: bool Whether the input should be converted to Lab colorspace prior to segmentation. For this purpose, the input is assumed to be RGB. Highly recommended. @@ -43,7 +43,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, convert2lab=Tru image = np.atleast_3d(image) if image.shape[2] != 3: ValueError("Only 3-channel 2d images are supported.") - image = ndimage.gaussian_filter(img_as_float(image), sigma) + image = ndimage.gaussian_filter(img_as_float(image), [sigma, sigma, 0]) if convert2lab: image = rgb2lab(image) From e65d6f6635b5d8289e2b5e5ac6565ea2fa3553bc Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 4 Aug 2012 20:27:31 +0100 Subject: [PATCH 196/648] Added smoothing option to quickshift --- skimage/segmentation/quickshift.pyx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index 3fcc94f7..be805a37 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -3,6 +3,7 @@ cimport numpy as np cimport cython from itertools import product +from scipy import ndimage from ..util import img_as_float from ..color import rgb2lab @@ -15,7 +16,7 @@ cdef extern from "math.h": @cython.boundscheck(False) @cython.wraparound(False) @cython.cdivision(True) -def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, convert2lab=True, random_seed=None): +def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, sigma=0, convert2lab=True, random_seed=None): """Segments image using quickshift clustering in Color-(x,y) space. Produces an oversegmentation of the image using the quickshift mode-seeking algorithm. @@ -34,7 +35,9 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, c Cut-off point for data distances. Higher means less clusters. return_tree: bool - Whether to return the full segmentation hierarchy tree + Whether to return the full segmentation hierarchy tree and distances. + sigma: float + Width for Gaussian smoothing as preprocessing. Zero means no smoothing. convert2lab: bool Whether the input should be converted to Lab colorspace prior to segmentation. For this purpose, the input is assumed to be RGB. @@ -64,6 +67,7 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, c ValueError("Only RGB images can be converted to Lab space.") image = rgb2lab(image) + image = ndimage.gaussian_filter(img_as_float(image), [sigma, sigma, 0]) cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c = np.ascontiguousarray(image) * ratio if random_seed is None: From ec6a1769faa51a8f6977c657e137c6c8d028f804 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 5 Aug 2012 13:51:55 +0100 Subject: [PATCH 197/648] merge segmentation examples --- .../plot_felzenszwalb_segmentation.py | 51 ----------- doc/examples/plot_quickshift.py | 43 ---------- doc/examples/plot_segmentations.py | 85 +++++++++++++++++++ doc/examples/plot_slic.py | 20 ----- 4 files changed, 85 insertions(+), 114 deletions(-) delete mode 100644 doc/examples/plot_felzenszwalb_segmentation.py delete mode 100644 doc/examples/plot_quickshift.py create mode 100644 doc/examples/plot_segmentations.py delete mode 100644 doc/examples/plot_slic.py diff --git a/doc/examples/plot_felzenszwalb_segmentation.py b/doc/examples/plot_felzenszwalb_segmentation.py deleted file mode 100644 index 18ac0f80..00000000 --- a/doc/examples/plot_felzenszwalb_segmentation.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -================================================= -Felzenszwalb's efficient graph based segmentation -================================================= - -This fast 2d image segmentation algorithm, proposed in [1]_ is popular in the -computer vision community. It is often used to extract "superpixels", small -homogeneous image regions, which build the basis for further processing. - -The algorithm has a single ``scale`` parameter that influences the segment -size. The actual size and number of segments can vary greatly, depending on -local contrast. - -.. [1] Efficient graph-based image segmentation, Felzenszwalb, P.F. and - Huttenlocher, D.P. International Journal of Computer Vision, 2004 -""" -print __doc__ - -import matplotlib.pyplot as plt -import numpy as np - -from skimage.data import lena -from skimage.segmentation import felzenszwalb_segmentation -from skimage.util import img_as_float - -img = img_as_float(lena()) -segments = felzenszwalb_segmentation(img, scale=1) -segments = np.unique(segments, return_inverse=True)[1].reshape(img.shape[:2]) - -print("number of segments: %d" % len(np.unique(segments))) - - -fig, (ax_org, ax_sp, ax_mean) = plt.subplots(1, 3) -ax_org.set_title("original") -ax_org.imshow(img, interpolation='nearest') -ax_org.axis("off") - -ax_sp.set_title("superpixels") -ax_sp.imshow(segments, interpolation='nearest', cmap=plt.cm.prism) -ax_sp.axis("off") - -colors = [np.bincount(segments.ravel(), img[:, :, c].ravel()) for c in - xrange(img.shape[2])] -counts = np.bincount(segments.ravel()) -colors = np.vstack(colors) / counts -ax_mean.set_title("mean color") -ax_mean.imshow(colors.T[segments], interpolation='nearest') -ax_mean.axis("off") -fig.subplots_adjust(wspace=0.02, hspace=0.02, top=0.9, - bottom=0.02, left=0.02, right=0.98) -plt.show() diff --git a/doc/examples/plot_quickshift.py b/doc/examples/plot_quickshift.py deleted file mode 100644 index 9b096459..00000000 --- a/doc/examples/plot_quickshift.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -============================= -Quickshift image segmentation -============================= - -Quickshift is a relatively recent 2d image segmentation algorithm, based on an -approximation of kernelized mean-shift. Therefore it belongs to the family -of local mode-seeking algorithms and is applied to the color+coordinate space, -see [1]_ It is often used to extract "superpixels", small homogeneous image -regions, which build the basis for further processing. - -One of the benefits of quickshift is that it actually computes a -hierarchical segmentation on multiple scales simultaneously. - -Quickshift has two parameters, one controlling the scale of the local -density approximation, the other selecting a level in the hierarchical -segmentation that is produced. - -.. [1] Quick shift and kernel methods for mode seeking, Vedaldi, A. and Soatto, S. - European Conference on Computer Vision, 2008 -""" -print __doc__ - -import matplotlib.pyplot as plt -import numpy as np - -from skimage.data import lena -from skimage.segmentation import quickshift, visualize_boundaries -from skimage.util import img_as_float -from skimage.color import rgb2lab - -img = img_as_float(lena())[::2, ::2, :].copy("C") -segments = quickshift(rgb2lab(img), kernel_size=5, max_dist=20) -segments_rgb = quickshift(img, kernel_size=5, max_dist=20) - -print("number of segments: %d" % len(np.unique(segments))) -boundaries = visualize_boundaries(img, segments) -boundaries_rgb = visualize_boundaries(img, segments_rgb) -plt.imshow(boundaries) -plt.figure() -plt.imshow(boundaries_rgb) -plt.axis("off") -plt.show() diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py new file mode 100644 index 00000000..b204290e --- /dev/null +++ b/doc/examples/plot_segmentations.py @@ -0,0 +1,85 @@ +""" +==================================================== +Comparison of segmentation and superpixel algorithms +==================================================== + +This example compares three popular low-level image segmentation methods. +As it is difficult do obtain good segmentations, and the definition of "good" +often depends on the application, these methods are usually used +for optaining an oversegmentation, also known as superpixels. These superpixels +then serve as the level of operation for more sophisticated algorithms such as CRFs. + + + +Felzenszwalb's efficient graph based segmentation +------------------------------------------------- +This fast 2d image segmentation algorithm, proposed in [1]_ is popular in the +computer vision community. +The algorithm has a single ``scale`` parameter that influences the segment +size. The actual size and number of segments can vary greatly, depending on +local contrast. + +.. [1] Efficient graph-based image segmentation, Felzenszwalb, P.F. and + Huttenlocher, D.P. International Journal of Computer Vision, 2004 + + +Quickshift image segmentation +----------------------------- + +Quickshift is a relatively recent 2d image segmentation algorithm, based on an +approximation of kernelized mean-shift. Therefore it belongs to the family +of local mode-seeking algorithms and is applied to the color+coordinate space, +see [2]_. + +One of the benefits of quickshift is that it actually computes a +hierarchical segmentation on multiple scales simultaneously. + +Quickshift has two parameters, one controlling the scale of the local +density approximation, the other selecting a level in the hierarchical +segmentation that is produced. + +.. [2] Quick shift and kernel methods for mode seeking, + Vedaldi, A. and Soatto, S. + European Conference on Computer Vision, 2008 + + +SLIC - K-Means based image segmentation +--------------------------------------- +This algorithm simply performs K-kmeans in the 5d color-coordinate space and is +therefore closely related to quickshift. As the clustering method is simpler, +it is very efficient. It is essential for this algorithm to work in Lab color +space to obtain good results. The algorithm quickly gained momentum and is now +widely used. See [3] for details. + +.. [3] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, + Pascal Fua, and Sabine Suesstrunk, SLIC Superpixels Compared to + State-of-the-art Superpixel Methods, TPAMI, May 2012. +""" +print __doc__ + +import matplotlib.pyplot as plt +import numpy as np + +from skimage.data import lena +from skimage.segmentation import felzenszwalb_segmentation, \ + visualize_boundaries, slic, quickshift +from skimage.util import img_as_float + +img = img_as_float(lena()[::2, ::2]) +segments_fz = felzenszwalb_segmentation(img, scale=100, sigma=0.5, min_size=50) +segments_slic = slic(img, ratio=10, n_segments=250, sigma=1) +segments_quick = quickshift(img, kernel_size=3, max_dist=6, ratio=0.5) + +print("Felzenszwalb's number of segments: %d" % len(np.unique(segments_fz))) +print("Slic number of segments: %d" % len(np.unique(segments_slic))) +print("Quickshift number of segments: %d" % len(np.unique(segments_quick))) + +fig, ax = plt.subplots(1, 3) + +ax[0].imshow(visualize_boundaries(img, segments_fz)) +ax[1].imshow(visualize_boundaries(img, segments_slic)) +ax[2].imshow(visualize_boundaries(img, segments_quick)) +for a in ax: + a.set_xticks(()) + a.set_yticks(()) +plt.show() diff --git a/doc/examples/plot_slic.py b/doc/examples/plot_slic.py deleted file mode 100644 index 8ff59d98..00000000 --- a/doc/examples/plot_slic.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -""" -print __doc__ - -import matplotlib.pyplot as plt -import numpy as np - -from skimage.data import lena -from skimage.segmentation import slic, visualize_boundaries -from skimage.util import img_as_float - -img = img_as_float(lena()).copy("C") -segments = slic(img, ratio=10.0, n_segments=1000) - -print("number of segments: %d" % len(np.unique(segments))) - -boundaries_mine = visualize_boundaries(img, segments) -plt.imshow(boundaries_mine) -plt.axis("off") -plt.show() From b1eb4265ed5704d8c77d83e9972a6c042879df76 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 5 Aug 2012 13:52:15 +0100 Subject: [PATCH 198/648] Post-processing for felzenszwalbs algorithm. --- skimage/segmentation/_felzenszwalb.pyx | 16 ++++++++++++++-- skimage/segmentation/felzenszwalb.py | 7 +++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/skimage/segmentation/_felzenszwalb.pyx b/skimage/segmentation/_felzenszwalb.pyx index a677020a..b66fbd16 100644 --- a/skimage/segmentation/_felzenszwalb.pyx +++ b/skimage/segmentation/_felzenszwalb.pyx @@ -7,7 +7,7 @@ from skimage.morphology.ccomp cimport find_root, join_trees from ..util import img_as_float -def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8): +def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8, min_size=20): """Computes Felsenszwalb's efficient graph based segmentation for a single channel. Produces an oversegmentation of a 2d image using a fast, minimum spanning @@ -28,6 +28,8 @@ def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8): Free parameter. Higher means larger clusters. sigma: float Width of Gaussian kernel used in preprocessing. + min_size: int + Minimum component size. Enforced using postprocessing. Returns ------- @@ -38,7 +40,8 @@ def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8): raise ValueError("This algorithm works only on single-channel 2d images." "Got image of shape %s" % str(image.shape)) image = img_as_float(image) - scale = float(scale) + # rescale scale to behave like in reference implementation + scale = float(scale) / 255. image = scipy.ndimage.gaussian_filter(image, sigma=sigma) # compute edge weights in 8 connectivity: @@ -88,6 +91,15 @@ def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8): segment_size[seg_new] = segment_size[seg0] + segment_size[seg1] cint[seg_new] = costs_p[0] + # postprocessing to remove small segments + edges_p = edges.data + for e in range(costs.size): + seg0 = find_root(segments_p, edges_p[0]) + seg1 = find_root(segments_p, edges_p[1]) + edges_p += 2 + if segment_size[seg0] < min_size or segment_size[seg1] < min_size: + join_trees(segments_p, seg0, seg1) + # unravel the union find tree flat = segments.ravel() old = np.zeros_like(flat) diff --git a/skimage/segmentation/felzenszwalb.py b/skimage/segmentation/felzenszwalb.py index cfcf4f23..7da5863a 100644 --- a/skimage/segmentation/felzenszwalb.py +++ b/skimage/segmentation/felzenszwalb.py @@ -4,7 +4,7 @@ import numpy as np from ._felzenszwalb import _felzenszwalb_segmentation_grey -def felzenszwalb_segmentation(image, scale=1, sigma=0.8): +def felzenszwalb_segmentation(image, scale=1, sigma=0.8, min_size=20): """Computes Felsenszwalb's efficient graph based image segmentation. Produces an oversegmentation of a multichannel (i.e. RGB) image @@ -29,6 +29,8 @@ def felzenszwalb_segmentation(image, scale=1, sigma=0.8): Free parameter. Higher means larger clusters. sigma: float Width of Gaussian kernel used in preprocessing. + min_size: int + Minimum component size. Enforced using postprocessing. Returns ------- @@ -59,7 +61,8 @@ def felzenszwalb_segmentation(image, scale=1, sigma=0.8): # compute quickshift for each channel for c in xrange(n_channels): channel = np.ascontiguousarray(image[:, :, c]) - s = _felzenszwalb_segmentation_grey(channel, scale=scale, sigma=sigma) + s = _felzenszwalb_segmentation_grey(channel, scale=scale, sigma=sigma, + min_size=min_size) segmentations.append(s) # put pixels in same segment only if in the same segment in all images From f22ae2871a4b50dbab76f2ea99cebb4b25d3df6c Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 5 Aug 2012 13:54:02 +0100 Subject: [PATCH 199/648] Pep8 --- doc/examples/plot_segmentations.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py index b204290e..9ae45d60 100644 --- a/doc/examples/plot_segmentations.py +++ b/doc/examples/plot_segmentations.py @@ -3,12 +3,11 @@ Comparison of segmentation and superpixel algorithms ==================================================== -This example compares three popular low-level image segmentation methods. -As it is difficult do obtain good segmentations, and the definition of "good" -often depends on the application, these methods are usually used -for optaining an oversegmentation, also known as superpixels. These superpixels -then serve as the level of operation for more sophisticated algorithms such as CRFs. - +This example compares three popular low-level image segmentation methods. As +it is difficult do obtain good segmentations, and the definition of "good" +often depends on the application, these methods are usually used for optaining +an oversegmentation, also known as superpixels. These superpixels then serve as +the level of operation for more sophisticated algorithms such as CRFs. Felzenszwalb's efficient graph based segmentation From 809871803697e721779be24bad84753356c0838e Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 5 Aug 2012 14:33:30 +0100 Subject: [PATCH 200/648] Fixup tests, add test for slic. --- skimage/segmentation/tests/test_quickshift.py | 19 +++++++------- skimage/segmentation/tests/test_slic.py | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 skimage/segmentation/tests/test_slic.py diff --git a/skimage/segmentation/tests/test_quickshift.py b/skimage/segmentation/tests/test_quickshift.py index a904837c..b4bc1e86 100644 --- a/skimage/segmentation/tests/test_quickshift.py +++ b/skimage/segmentation/tests/test_quickshift.py @@ -7,17 +7,17 @@ from skimage.segmentation import quickshift def test_grey(): rnd = np.random.RandomState(0) img = np.zeros((20, 20)) - img[:10, :10] = 0.2 + img[:10, 10:] = 0.2 img[10:, :10] = 0.4 img[10:, 10:] = 0.6 img += 0.1 * rnd.normal(size=img.shape) - seg = quickshift(img, random_seed=0) + seg = quickshift(img, kernel_size=2, max_dist=3, random_seed=0, convert2lab=False, sigma=0) # we expect 4 segments: assert_equal(len(np.unique(seg)), 4) # that mostly respect the 4 regions: for i in xrange(4): hist = np.histogram(img[seg == i], bins=[0, 0.1, 0.3, 0.5, 1])[0] - assert_greater(hist[i], 40) + assert_greater(hist[i], 20) def test_color(): @@ -26,20 +26,21 @@ def test_color(): img[:10, :10, 0] = 1 img[10:, :10, 1] = 1 img[10:, 10:, 2] = 1 - img += 0.2 * rnd.normal(size=img.shape) + img += 0.01 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = quickshift(img, random_seed=0) + seg = quickshift(img, random_seed=0, max_dist=30, kernel_size=10, sigma=0) # we expect 4 segments: assert_equal(len(np.unique(seg)), 4) assert_array_equal(seg[:10, :10], 0) - assert_array_equal(seg[10:, :10], 3) + assert_array_equal(seg[10:, :10], 2) assert_array_equal(seg[:10, 10:], 1) - assert_array_equal(seg[10:, 10:], 2) + assert_array_equal(seg[10:, 10:], 3) - seg2 = quickshift(img, kernel_size=1, max_dist=3, random_seed=0) + seg2 = quickshift(img, kernel_size=1, max_dist=2, random_seed=0, + convert2lab=False, sigma=0) # very oversegmented: - assert_equal(len(np.unique(seg2)), 18) + assert_equal(len(np.unique(seg2)), 11) # still don't cross lines assert_true((seg2[9, :] != seg2[10, :]).all()) assert_true((seg2[:, 9] != seg2[:, 10]).all()) diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py new file mode 100644 index 00000000..b4d4233a --- /dev/null +++ b/skimage/segmentation/tests/test_slic.py @@ -0,0 +1,26 @@ +import numpy as np +from numpy.testing import assert_equal, assert_array_equal +from skimage.segmentation import slic + + +def test_color(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 20, 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 + seg = slic(img, sigma=0, n_segments=4) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + assert_array_equal(seg[:10, :10], 0) + assert_array_equal(seg[10:, :10], 2) + assert_array_equal(seg[:10, 10:], 1) + assert_array_equal(seg[10:, 10:], 3) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() From 1db4ebcecb3052386f2d4d30174816a313a94c82 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 5 Aug 2012 20:25:26 +0100 Subject: [PATCH 201/648] MISC some typos in Example, titles set. --- doc/examples/plot_segmentations.py | 34 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py index 9ae45d60..0435d7e5 100644 --- a/doc/examples/plot_segmentations.py +++ b/doc/examples/plot_segmentations.py @@ -4,10 +4,10 @@ Comparison of segmentation and superpixel algorithms ==================================================== This example compares three popular low-level image segmentation methods. As -it is difficult do obtain good segmentations, and the definition of "good" -often depends on the application, these methods are usually used for optaining +it is difficult to obtain good segmentations, and the definition of "good" +often depends on the application, these methods are usually used for obtaining an oversegmentation, also known as superpixels. These superpixels then serve as -the level of operation for more sophisticated algorithms such as CRFs. +a basis for more sophisticated algorithms such as CRFs. Felzenszwalb's efficient graph based segmentation @@ -26,16 +26,17 @@ Quickshift image segmentation ----------------------------- Quickshift is a relatively recent 2d image segmentation algorithm, based on an -approximation of kernelized mean-shift. Therefore it belongs to the family -of local mode-seeking algorithms and is applied to the color+coordinate space, -see [2]_. +approximation of kernelized mean-shift. Therefore it belongs to the family of +local mode-seeking algorithms and is applied to the 5d space consisting of +color information and image location. see [2]_. One of the benefits of quickshift is that it actually computes a hierarchical segmentation on multiple scales simultaneously. -Quickshift has two parameters, one controlling the scale of the local -density approximation, the other selecting a level in the hierarchical -segmentation that is produced. +Quickshift has three parameters: ``sigma`` controls the scale of the local +density approximation, ``max_dist`` other selecting a level in the hierarchical +segmentation that is produced. There is also a trade-off between distance in +color-space and distance in image-space, given by ``ratio``. .. [2] Quick shift and kernel methods for mode seeking, Vedaldi, A. and Soatto, S. @@ -44,11 +45,13 @@ segmentation that is produced. SLIC - K-Means based image segmentation --------------------------------------- -This algorithm simply performs K-kmeans in the 5d color-coordinate space and is -therefore closely related to quickshift. As the clustering method is simpler, -it is very efficient. It is essential for this algorithm to work in Lab color -space to obtain good results. The algorithm quickly gained momentum and is now -widely used. See [3] for details. +This algorithm simply performs K-kmeans in the 5d space of color information +and image location and is therefore closely related to quickshift. As the +clustering method is simpler, it is very efficient. It is essential for this +algorithm to work in Lab color space to obtain good results. The algorithm +quickly gained momentum and is now widely used. See [3] for details. The +``ratio`` parameter trades off color-similarity and proximity, as in the case +of Quickshift, while ``n_segments`` chooses the number of centers for kmeans. .. [3] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, Pascal Fua, and Sabine Suesstrunk, SLIC Superpixels Compared to @@ -76,8 +79,11 @@ print("Quickshift number of segments: %d" % len(np.unique(segments_quick))) fig, ax = plt.subplots(1, 3) ax[0].imshow(visualize_boundaries(img, segments_fz)) +ax[0].set_title("Felzenszwalbs's method") ax[1].imshow(visualize_boundaries(img, segments_slic)) +ax[1].set_title("SLIC") ax[2].imshow(visualize_boundaries(img, segments_quick)) +ax[2].set_title("Quickshift") for a in ax: a.set_xticks(()) a.set_yticks(()) From 530f2c31c402a0609cad80481e5d9ef258ac2599 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 5 Aug 2012 20:25:34 +0100 Subject: [PATCH 202/648] pep8 --- skimage/__init__.py | 1 - skimage/filter/tests/test_thresholding.py | 2 -- skimage/segmentation/__init__.py | 3 -- skimage/segmentation/_felzenszwalb.pyx | 20 +++++++------ skimage/segmentation/quickshift.pyx | 35 ++++++++++++++--------- skimage/segmentation/slic.pyx | 31 ++++++++++++-------- 6 files changed, 53 insertions(+), 39 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index b1c331ff..ad37f425 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -88,7 +88,6 @@ test = _setup_test() test_verbose = _setup_test(verbose=True) - def get_log(name=None): """Return a console logger. diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index d17f9b84..97d3d9e3 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -77,13 +77,11 @@ def test_otsu_camera_image(): assert 86 < threshold_otsu(camera) < 88 - def test_otsu_coins_image(): coins = skimage.img_as_ubyte(data.coins()) assert 106 < threshold_otsu(coins) < 108 - def test_otsu_coins_image_as_float(): coins = skimage.img_as_float(data.coins()) assert 0.41 < threshold_otsu(coins) < 0.42 diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 2fb6902c..b1e6f783 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -3,6 +3,3 @@ from .felzenszwalb import felzenszwalb_segmentation from .slic import slic from .quickshift import quickshift from .boundaries import find_boundaries, visualize_boundaries - -__all__ = [random_walker, quickshift, felzenszwalb_segmentation, - slic, find_boundaries, visualize_boundaries] diff --git a/skimage/segmentation/_felzenszwalb.pyx b/skimage/segmentation/_felzenszwalb.pyx index b66fbd16..f4069274 100644 --- a/skimage/segmentation/_felzenszwalb.pyx +++ b/skimage/segmentation/_felzenszwalb.pyx @@ -8,7 +8,7 @@ from ..util import img_as_float def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8, min_size=20): - """Computes Felsenszwalb's efficient graph based segmentation for a single channel. + """Felzenszwalb's efficient graph based segmentation for a single channel. Produces an oversegmentation of a 2d image using a fast, minimum spanning tree based clustering on the image grid. The parameter ``scale`` sets an @@ -37,8 +37,8 @@ def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8, min_size=20): Integer mask indicating segment labels. """ if image.ndim != 2: - raise ValueError("This algorithm works only on single-channel 2d images." - "Got image of shape %s" % str(image.shape)) + raise ValueError("This algorithm works only on single-channel 2d" + "images. Got image of shape %s" % str(image.shape)) image = img_as_float(image) # rescale scale to behave like in reference implementation scale = float(scale) / 255. @@ -49,16 +49,19 @@ def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8, min_size=20): down_cost = np.abs((image[:, 1:] - image[:, :-1])) dright_cost = np.abs((image[1:, 1:] - image[:-1, :-1])) uright_cost = np.abs((image[1:, :-1] - image[:-1, 1:])) - cdef np.ndarray[np.float_t, ndim=1] costs = np.hstack([right_cost.ravel(), down_cost.ravel(), - dright_cost.ravel(), uright_cost.ravel()]).astype(np.float) + cdef np.ndarray[np.float_t, ndim=1] costs = np.hstack([right_cost.ravel(), + down_cost.ravel(), dright_cost.ravel(), + uright_cost.ravel()]).astype(np.float) # compute edges between pixels: width, height = image.shape[:2] - cdef np.ndarray[np.int_t, ndim=2] segments = np.arange(width * height).reshape(width, height) + cdef np.ndarray[np.int_t, ndim=2] segments \ + = np.arange(width * height).reshape(width, height) right_edges = np.c_[segments[1:, :].ravel(), segments[:-1, :].ravel()] down_edges = np.c_[segments[:, 1:].ravel(), segments[:, :-1].ravel()] dright_edges = np.c_[segments[1:, 1:].ravel(), segments[:-1, :-1].ravel()] uright_edges = np.c_[segments[:-1, 1:].ravel(), segments[1:, :-1].ravel()] - cdef np.ndarray[np.int_t, ndim=2] edges = np.vstack([right_edges, down_edges, dright_edges, uright_edges]) + cdef np.ndarray[np.int_t, ndim=2] edges \ + = np.vstack([right_edges, down_edges, dright_edges, uright_edges]) # initialize data structures for segment size # and inner cost, then start greedy iteration over edges. edge_queue = np.argsort(costs) @@ -67,7 +70,8 @@ def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8, min_size=20): cdef np.int_t *segments_p = segments.data cdef np.int_t *edges_p = edges.data cdef np.float_t *costs_p = costs.data - cdef np.ndarray[np.int_t, ndim=1] segment_size = np.ones(width * height, dtype=np.int) + cdef np.ndarray[np.int_t, ndim=1] segment_size \ + = np.ones(width * height, dtype=np.int) # inner cost of segments cdef np.ndarray[np.float_t, ndim=1] cint = np.zeros(width * height) cdef int seg0, seg1, seg_new diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index be805a37..37c9294b 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -16,14 +16,16 @@ cdef extern from "math.h": @cython.boundscheck(False) @cython.wraparound(False) @cython.cdivision(True) -def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, sigma=0, convert2lab=True, random_seed=None): +def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, + sigma=0, convert2lab=True, random_seed=None): """Segments image using quickshift clustering in Color-(x,y) space. - Produces an oversegmentation of the image using the quickshift mode-seeking algorithm. + Produces an oversegmentation of the image using the quickshift mode-seeking + algorithm. Parameters ---------- - image: (width, height, channels) ndarray + image: (width, height, channels) ndarray Input image ratio: float, between 0 and 1. Balances color-space proximity and image-space proximity. @@ -39,8 +41,8 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, s sigma: float Width for Gaussian smoothing as preprocessing. Zero means no smoothing. convert2lab: bool - Whether the input should be converted to Lab colorspace prior to segmentation. - For this purpose, the input is assumed to be RGB. + Whether the input should be converted to Lab colorspace prior to + segmentation. For this purpose, the input is assumed to be RGB. random_seed: None or int Random seed used for breaking ties @@ -51,12 +53,14 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, s Notes ----- - The authors advocate to convert the image to Lab color space prior to segmentation, though - this is not strictly necessary. For this to work, the image must be given in RGB format. + The authors advocate to convert the image to Lab color space prior to + segmentation, though this is not strictly necessary. For this to work, the + image must be given in RGB format. References ---------- - .. [1] Quick shift and kernel methods for mode seeking, Vedaldi, A. and Soatto, S. + .. [1] Quick shift and kernel methods for mode seeking, + Vedaldi, A. and Soatto, S. European Conference on Computer Vision, 2008 @@ -68,7 +72,8 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, s image = rgb2lab(image) image = ndimage.gaussian_filter(img_as_float(image), [sigma, sigma, 0]) - cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c = np.ascontiguousarray(image) * ratio + cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c \ + = np.ascontiguousarray(image) * ratio if random_seed is None: random_state = np.random.RandomState() @@ -98,7 +103,8 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, s cdef np.float_t* image_p = image_c.data cdef np.float_t* current_pixel_p = image_p - cdef np.ndarray[dtype=np.float_t, ndim=2] densities = np.zeros((width, height)) + cdef np.ndarray[dtype=np.float_t, ndim=2] densities \ + = np.zeros((width, height)) # compute densities for x, y in product(xrange(width), xrange(height)): x_min, x_max = max(x - w, 0), min(x + w + 1, width) @@ -115,8 +121,10 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, s densities += random_state.normal(scale=0.00001, size=(width, height)) # default parent to self: - cdef np.ndarray[dtype=np.int_t, ndim=2] parent = np.arange(width * height).reshape(width, height) - cdef np.ndarray[dtype=np.float_t, ndim=2] dist_parent = np.zeros((width, height)) + cdef np.ndarray[dtype=np.int_t, ndim=2] parent \ + = np.arange(width * height).reshape(width, height) + cdef np.ndarray[dtype=np.float_t, ndim=2] dist_parent \ + = np.zeros((width, height)) # find nearest node with higher density current_pixel_p = image_p for x, y in product(xrange(width), xrange(height)): @@ -139,7 +147,8 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, s dist_parent_flat = dist_parent.ravel() flat = parent.ravel() # remove parents with distance > max_dist - flat[dist_parent_flat > max_dist] = np.arange(width * height)[dist_parent_flat > max_dist] + too_far = dist_parent_flat > max_dist + flat[too_far] = np.arange(width * height)[too_far] old = np.zeros_like(flat) # flatten forest (mark each pixel with root of corresponding tree) while (old != flat).any(): diff --git a/skimage/segmentation/slic.pyx b/skimage/segmentation/slic.pyx index 37c6b79c..0d0adc49 100644 --- a/skimage/segmentation/slic.pyx +++ b/skimage/segmentation/slic.pyx @@ -6,7 +6,8 @@ from ..util import img_as_float from ..color import rgb2lab -def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, convert2lab=True): +def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, + convert2lab=True): """Segments image using k-means clustering in Color-(x,y) space. Parameters @@ -19,10 +20,12 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, convert2lab=Tru max_iter: int maximum number of iterations of k-means sigma: float - Width of Gaussian smoothing kernel for preprocessing. Zero means no smoothing. + Width of Gaussian smoothing kernel for preprocessing. Zero means no + smoothing. convert2lab: bool - Whether the input should be converted to Lab colorspace prior to segmentation. - For this purpose, the input is assumed to be RGB. Highly recommended. + Whether the input should be converted to Lab colorspace prior to + segmentation. For this purpose, the input is assumed to be RGB. Highly + recommended. Returns ------- @@ -57,19 +60,23 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, convert2lab=Tru n_seeds = len(means_y) means_color = np.zeros((n_seeds, n_seeds, 3)) - cdef np.ndarray[dtype=np.float_t, ndim=2] means = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) + cdef np.ndarray[dtype=np.float_t, ndim=2] means \ + = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) cdef np.float_t* current_mean cdef np.float_t* mean_entry n_means = means.shape[0] # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning ratio = (ratio / float(step)) ** 2 - cdef np.ndarray[dtype=np.float_t, ndim=3] image_yx = np.dstack([grid_y, grid_x, image / ratio]).copy("C") + cdef np.ndarray[dtype=np.float_t, ndim=3] image_yx \ + = np.dstack([grid_y, grid_x, image / ratio]).copy("C") cdef int i, k, x, y, x_min, x_max, y_min, y_max, changes cdef double dist_mean - cdef np.ndarray[dtype=np.int_t, ndim=2] nearest_mean = np.zeros((height, width), dtype=np.int) - cdef np.ndarray[dtype=np.float_t, ndim=2] distance = np.empty((height, width)) + cdef np.ndarray[dtype=np.int_t, ndim=2] nearest_mean \ + = np.zeros((height, width), dtype=np.int) + cdef np.ndarray[dtype=np.float_t, ndim=2] distance \ + = np.empty((height, width)) cdef np.float_t* image_p = image_yx.data cdef np.float_t* distance_p = distance.data cdef np.float_t* current_distance @@ -93,8 +100,8 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, convert2lab=Tru mean_entry = current_mean dist_mean = 0 for c in range(5): - # you would think the compiler can optimize this itself. - # mine can't (with O2) + # you would think the compiler can optimize this + # itself. mine can't (with O2) tmp = current_pixel[0] - mean_entry[0] dist_mean += tmp * tmp current_pixel += 1 @@ -109,8 +116,8 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, convert2lab=Tru if changes == 0: break # recompute means: - means_list = [np.bincount(nearest_mean.ravel(), image_yx[:, :, j].ravel()) - for j in xrange(5)] + means_list = [np.bincount(nearest_mean.ravel(), + image_yx[:, :, j].ravel()) for j in xrange(5)] in_mean = np.bincount(nearest_mean.ravel()) in_mean[in_mean == 0] = 1 means = (np.vstack(means_list) / in_mean).T.copy("C") From e0034e384a050cdb8ea2a55fbd10fa60f9b66665 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 5 Aug 2012 20:30:43 +0100 Subject: [PATCH 203/648] Minor fixes addressing @tonysyu's comments. --- skimage/segmentation/_felzenszwalb.pyx | 12 ++++-------- skimage/segmentation/quickshift.pyx | 7 ++----- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/skimage/segmentation/_felzenszwalb.pyx b/skimage/segmentation/_felzenszwalb.pyx index f4069274..15b528ad 100644 --- a/skimage/segmentation/_felzenszwalb.pyx +++ b/skimage/segmentation/_felzenszwalb.pyx @@ -11,21 +11,17 @@ def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8, min_size=20): """Felzenszwalb's efficient graph based segmentation for a single channel. Produces an oversegmentation of a 2d image using a fast, minimum spanning - tree based clustering on the image grid. The parameter ``scale`` sets an - observation level. Higher scale means less and larger segments. ``sigma`` - is the diameter of a Gaussian kernel, used for smoothing the image prior to - segmentation. - + tree based clustering on the image grid. The number of produced segments as well as their size can only be controlled indirectly through ``scale``. Segment size within an image can vary greatly depending on local contrast. Parameters ---------- - image: (width, height) ndarray + image: ndarray Input image scale: float - Free parameter. Higher means larger clusters. + Sets the obervation level. Higher means larger clusters. sigma: float Width of Gaussian kernel used in preprocessing. min_size: int @@ -33,7 +29,7 @@ def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8, min_size=20): Returns ------- - segment_mask: ndarray, [width, height] + segment_mask: (height, width) ndarray Integer mask indicating segment labels. """ if image.ndim != 2: diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index 37c9294b..a3c5ec9f 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -75,15 +75,12 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, cdef np.ndarray[dtype=np.float_t, ndim=3, mode="c"] image_c \ = np.ascontiguousarray(image) * ratio - if random_seed is None: - random_state = np.random.RandomState() - else: - random_state = np.random.RandomState(random_seed) + random_state = np.random.RandomState(random_seed) # We compute the distances twice since otherwise # we get crazy memory overhead (width * height * windowsize**2) - # TODO join orphant roots? + # TODO join orphaned roots? # Some nodes might not have a point of higher density within the # search window. We could do a global search over these in the end. # Reference implementation doesn't do that, though, and it only has From 73dd46019b4eb7934d4a96a2d23430f07a717b82 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 5 Aug 2012 21:06:37 +0100 Subject: [PATCH 204/648] FIX width/height trouble, add non-regression test --- skimage/segmentation/_felzenszwalb.pyx | 6 ++--- skimage/segmentation/quickshift.pyx | 26 +++++++++---------- skimage/segmentation/slic.pyx | 8 +++--- .../segmentation/tests/test_felzenszwalb.py | 4 +-- skimage/segmentation/tests/test_quickshift.py | 13 +++++----- skimage/segmentation/tests/test_slic.py | 3 ++- 6 files changed, 31 insertions(+), 29 deletions(-) diff --git a/skimage/segmentation/_felzenszwalb.pyx b/skimage/segmentation/_felzenszwalb.pyx index 15b528ad..5c1e097e 100644 --- a/skimage/segmentation/_felzenszwalb.pyx +++ b/skimage/segmentation/_felzenszwalb.pyx @@ -49,9 +49,9 @@ def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8, min_size=20): down_cost.ravel(), dright_cost.ravel(), uright_cost.ravel()]).astype(np.float) # compute edges between pixels: - width, height = image.shape[:2] + height, width = image.shape[:2] cdef np.ndarray[np.int_t, ndim=2] segments \ - = np.arange(width * height).reshape(width, height) + = np.arange(width * height).reshape(height, width) right_edges = np.c_[segments[1:, :].ravel(), segments[:-1, :].ravel()] down_edges = np.c_[segments[:, 1:].ravel(), segments[:, :-1].ravel()] dright_edges = np.c_[segments[1:, 1:].ravel(), segments[:-1, :-1].ravel()] @@ -107,4 +107,4 @@ def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8, min_size=20): old = flat flat = flat[flat] flat = np.unique(flat, return_inverse=True)[1] - return flat.reshape((width, height)) + return flat.reshape((height, width)) diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/quickshift.pyx index a3c5ec9f..dc4d86e1 100644 --- a/skimage/segmentation/quickshift.pyx +++ b/skimage/segmentation/quickshift.pyx @@ -91,8 +91,8 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, raise ValueError("Sigma should be >= 1") cdef int w = int(3 * kernel_size) - cdef int width = image_c.shape[0] - cdef int height = image_c.shape[1] + cdef int height = image_c.shape[0] + cdef int width = image_c.shape[1] cdef int channels = image_c.shape[2] cdef float closest, dist cdef int x, y, x_, y_ @@ -101,11 +101,11 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, cdef np.float_t* current_pixel_p = image_p cdef np.ndarray[dtype=np.float_t, ndim=2] densities \ - = np.zeros((width, height)) + = np.zeros((height, width)) # compute densities - for x, y in product(xrange(width), xrange(height)): - x_min, x_max = max(x - w, 0), min(x + w + 1, width) - y_min, y_max = max(y - w, 0), min(y + w + 1, height) + for x, y in product(xrange(height), xrange(width)): + x_min, x_max = max(x - w, 0), min(x + w + 1, height) + y_min, y_max = max(y - w, 0), min(y + w + 1, width) for x_, y_ in product(xrange(x_min, x_max), xrange(y_min, y_max)): dist = 0 for c in xrange(channels): @@ -115,20 +115,20 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, current_pixel_p += channels # this will break ties that otherwise would give us headache - densities += random_state.normal(scale=0.00001, size=(width, height)) + densities += random_state.normal(scale=0.00001, size=(height, width)) # default parent to self: cdef np.ndarray[dtype=np.int_t, ndim=2] parent \ - = np.arange(width * height).reshape(width, height) + = np.arange(width * height).reshape(height, width) cdef np.ndarray[dtype=np.float_t, ndim=2] dist_parent \ - = np.zeros((width, height)) + = np.zeros((height, width)) # find nearest node with higher density current_pixel_p = image_p - for x, y in product(xrange(width), xrange(height)): + for x, y in product(xrange(height), xrange(width)): current_density = densities[x, y] closest = np.inf - x_min, x_max = max(x - w, 0), min(x + w + 1, width) - y_min, y_max = max(y - w, 0), min(y + w + 1, height) + x_min, x_max = max(x - w, 0), min(x + w + 1, height) + y_min, y_max = max(y - w, 0), min(y + w + 1, width) for x_, y_ in product(xrange(x_min, x_max), xrange(y_min, y_max)): if densities[x_, y_] > current_density: dist = 0 @@ -152,7 +152,7 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, old = flat flat = flat[flat] flat = np.unique(flat, return_inverse=True)[1] - flat = flat.reshape(width, height) + flat = flat.reshape(height, width) if return_tree: return flat, parent, dist_parent return flat diff --git a/skimage/segmentation/slic.pyx b/skimage/segmentation/slic.pyx index 0d0adc49..652f977e 100644 --- a/skimage/segmentation/slic.pyx +++ b/skimage/segmentation/slic.pyx @@ -53,13 +53,13 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, # initialize on grid: height, width = image.shape[:2] # approximate grid size for desired n_segments - step = np.sqrt(height * width / n_segments) + step = np.ceil(np.sqrt(height * width / n_segments)) grid_y, grid_x = np.mgrid[:height, :width] means_y = grid_y[::step, ::step] means_x = grid_x[::step, ::step] + print(means_y, means_x) - n_seeds = len(means_y) - means_color = np.zeros((n_seeds, n_seeds, 3)) + means_color = np.zeros((means_y.shape[0], means_y.shape[1], 3)) cdef np.ndarray[dtype=np.float_t, ndim=2] means \ = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) cdef np.float_t* current_mean @@ -92,7 +92,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, y_min = int(max(current_mean[0] - 2 * step, 0)) y_max = int(min(current_mean[0] + 2 * step, height)) x_min = int(max(current_mean[1] - 2 * step, 0)) - x_max = int(min(current_mean[1] + 2 * step, height)) + x_max = int(min(current_mean[1] + 2 * step, width)) for y in xrange(y_min, y_max): current_pixel = &image_p[5 * (y * width + x_min)] current_distance = &distance_p[y * width + x_min] diff --git a/skimage/segmentation/tests/test_felzenszwalb.py b/skimage/segmentation/tests/test_felzenszwalb.py index f6cca31b..fe68c443 100644 --- a/skimage/segmentation/tests/test_felzenszwalb.py +++ b/skimage/segmentation/tests/test_felzenszwalb.py @@ -6,7 +6,7 @@ from skimage.segmentation import felzenszwalb_segmentation def test_grey(): # very weak tests. This algorithm is pretty unstable. - img = np.zeros((20, 20)) + img = np.zeros((20, 21)) img[:10, 10:] = 0.2 img[10:, :10] = 0.4 img[10:, 10:] = 0.6 @@ -21,7 +21,7 @@ def test_grey(): def test_color(): # very weak tests. This algorithm is pretty unstable. - img = np.zeros((20, 20, 3)) + img = np.zeros((20, 21, 3)) img[:10, :10, 0] = 1 img[10:, :10, 1] = 1 img[10:, 10:, 2] = 1 diff --git a/skimage/segmentation/tests/test_quickshift.py b/skimage/segmentation/tests/test_quickshift.py index b4bc1e86..5c6eb024 100644 --- a/skimage/segmentation/tests/test_quickshift.py +++ b/skimage/segmentation/tests/test_quickshift.py @@ -6,12 +6,13 @@ from skimage.segmentation import quickshift def test_grey(): rnd = np.random.RandomState(0) - img = np.zeros((20, 20)) + img = np.zeros((20, 21)) img[:10, 10:] = 0.2 img[10:, :10] = 0.4 img[10:, 10:] = 0.6 img += 0.1 * rnd.normal(size=img.shape) - seg = quickshift(img, kernel_size=2, max_dist=3, random_seed=0, convert2lab=False, sigma=0) + seg = quickshift(img, kernel_size=2, max_dist=3, random_seed=0, + convert2lab=False, sigma=0) # we expect 4 segments: assert_equal(len(np.unique(seg)), 4) # that mostly respect the 4 regions: @@ -22,7 +23,7 @@ def test_grey(): def test_color(): rnd = np.random.RandomState(0) - img = np.zeros((20, 20, 3)) + img = np.zeros((20, 21, 3)) img[:10, :10, 0] = 1 img[10:, :10, 1] = 1 img[10:, 10:, 2] = 1 @@ -33,14 +34,14 @@ def test_color(): # we expect 4 segments: assert_equal(len(np.unique(seg)), 4) assert_array_equal(seg[:10, :10], 0) - assert_array_equal(seg[10:, :10], 2) + assert_array_equal(seg[10:, :10], 3) assert_array_equal(seg[:10, 10:], 1) - assert_array_equal(seg[10:, 10:], 3) + assert_array_equal(seg[10:, 10:], 2) seg2 = quickshift(img, kernel_size=1, max_dist=2, random_seed=0, convert2lab=False, sigma=0) # very oversegmented: - assert_equal(len(np.unique(seg2)), 11) + assert_equal(len(np.unique(seg2)), 7) # still don't cross lines assert_true((seg2[9, :] != seg2[10, :]).all()) assert_true((seg2[:, 9] != seg2[:, 10]).all()) diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index b4d4233a..f2d6698d 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -5,7 +5,7 @@ from skimage.segmentation import slic def test_color(): rnd = np.random.RandomState(0) - img = np.zeros((20, 20, 3)) + img = np.zeros((20, 21, 3)) img[:10, :10, 0] = 1 img[10:, :10, 1] = 1 img[10:, 10:, 2] = 1 @@ -14,6 +14,7 @@ def test_color(): img[img < 0] = 0 seg = slic(img, sigma=0, n_segments=4) # we expect 4 segments: + print(seg) assert_equal(len(np.unique(seg)), 4) assert_array_equal(seg[:10, :10], 0) assert_array_equal(seg[10:, :10], 2) From f421587aa4a1e29b891380103f9ddce93aec32d5 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 5 Aug 2012 21:10:29 +0100 Subject: [PATCH 205/648] ENH renamed "felzenszwalb_segmentation" to "felzenszwalb", remove debug output from slic --- doc/examples/plot_segmentations.py | 4 ++-- skimage/segmentation/__init__.py | 2 +- skimage/segmentation/_felzenszwalb.pyx | 2 +- skimage/segmentation/felzenszwalb.py | 8 ++++---- skimage/segmentation/slic.pyx | 1 - 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py index 0435d7e5..890ffcce 100644 --- a/doc/examples/plot_segmentations.py +++ b/doc/examples/plot_segmentations.py @@ -63,12 +63,12 @@ import matplotlib.pyplot as plt import numpy as np from skimage.data import lena -from skimage.segmentation import felzenszwalb_segmentation, \ +from skimage.segmentation import felzenszwalb, \ visualize_boundaries, slic, quickshift from skimage.util import img_as_float img = img_as_float(lena()[::2, ::2]) -segments_fz = felzenszwalb_segmentation(img, scale=100, sigma=0.5, min_size=50) +segments_fz = felzenszwalb(img, scale=100, sigma=0.5, min_size=50) segments_slic = slic(img, ratio=10, n_segments=250, sigma=1) segments_quick = quickshift(img, kernel_size=3, max_dist=6, ratio=0.5) diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index b1e6f783..cb06108d 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1,5 +1,5 @@ from .random_walker_segmentation import random_walker -from .felzenszwalb import felzenszwalb_segmentation +from .felzenszwalb import felzenszwalb from .slic import slic from .quickshift import quickshift from .boundaries import find_boundaries, visualize_boundaries diff --git a/skimage/segmentation/_felzenszwalb.pyx b/skimage/segmentation/_felzenszwalb.pyx index 5c1e097e..76320d3a 100644 --- a/skimage/segmentation/_felzenszwalb.pyx +++ b/skimage/segmentation/_felzenszwalb.pyx @@ -7,7 +7,7 @@ from skimage.morphology.ccomp cimport find_root, join_trees from ..util import img_as_float -def _felzenszwalb_segmentation_grey(image, scale=1, sigma=0.8, min_size=20): +def _felzenszwalb_grey(image, scale=1, sigma=0.8, min_size=20): """Felzenszwalb's efficient graph based segmentation for a single channel. Produces an oversegmentation of a 2d image using a fast, minimum spanning diff --git a/skimage/segmentation/felzenszwalb.py b/skimage/segmentation/felzenszwalb.py index 7da5863a..cf79706d 100644 --- a/skimage/segmentation/felzenszwalb.py +++ b/skimage/segmentation/felzenszwalb.py @@ -1,10 +1,10 @@ import warnings import numpy as np -from ._felzenszwalb import _felzenszwalb_segmentation_grey +from ._felzenszwalb import _felzenszwalb_grey -def felzenszwalb_segmentation(image, scale=1, sigma=0.8, min_size=20): +def felzenszwalb(image, scale=1, sigma=0.8, min_size=20): """Computes Felsenszwalb's efficient graph based image segmentation. Produces an oversegmentation of a multichannel (i.e. RGB) image @@ -46,7 +46,7 @@ def felzenszwalb_segmentation(image, scale=1, sigma=0.8, min_size=20): #image = img_as_float(image) if image.ndim == 2: # assume single channel image - return _felzenszwalb_segmentation_grey(image, scale=scale, sigma=sigma) + return _felzenszwalb_grey(image, scale=scale, sigma=sigma) elif image.ndim != 3: raise ValueError("Got image with ndim=%d, don't know" @@ -61,7 +61,7 @@ def felzenszwalb_segmentation(image, scale=1, sigma=0.8, min_size=20): # compute quickshift for each channel for c in xrange(n_channels): channel = np.ascontiguousarray(image[:, :, c]) - s = _felzenszwalb_segmentation_grey(channel, scale=scale, sigma=sigma, + s = _felzenszwalb_grey(channel, scale=scale, sigma=sigma, min_size=min_size) segmentations.append(s) diff --git a/skimage/segmentation/slic.pyx b/skimage/segmentation/slic.pyx index 652f977e..98a6f6bc 100644 --- a/skimage/segmentation/slic.pyx +++ b/skimage/segmentation/slic.pyx @@ -57,7 +57,6 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, grid_y, grid_x = np.mgrid[:height, :width] means_y = grid_y[::step, ::step] means_x = grid_x[::step, ::step] - print(means_y, means_x) means_color = np.zeros((means_y.shape[0], means_y.shape[1], 3)) cdef np.ndarray[dtype=np.float_t, ndim=2] means \ From f56034630999f08a50a15e2687571b39d97294c6 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 5 Aug 2012 21:31:24 +0100 Subject: [PATCH 206/648] Trying to avoid name collisions. --- skimage/segmentation/__init__.py | 6 +++--- .../{felzenszwalb.py => _felzenszwalb.py} | 2 +- .../segmentation/{quickshift.pyx => _quickshift.pyx} | 0 skimage/segmentation/{slic.pyx => _slic.pyx} | 0 .../{_felzenszwalb.pyx => felzenszwalb_cy.pyx} | 0 skimage/segmentation/setup.py | 12 ++++++------ 6 files changed, 10 insertions(+), 10 deletions(-) rename skimage/segmentation/{felzenszwalb.py => _felzenszwalb.py} (98%) rename skimage/segmentation/{quickshift.pyx => _quickshift.pyx} (100%) rename skimage/segmentation/{slic.pyx => _slic.pyx} (100%) rename skimage/segmentation/{_felzenszwalb.pyx => felzenszwalb_cy.pyx} (100%) diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index cb06108d..69a580c6 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1,5 +1,5 @@ from .random_walker_segmentation import random_walker -from .felzenszwalb import felzenszwalb -from .slic import slic -from .quickshift import quickshift +from ._felzenszwalb import felzenszwalb +from ._slic import slic +from ._quickshift import quickshift from .boundaries import find_boundaries, visualize_boundaries diff --git a/skimage/segmentation/felzenszwalb.py b/skimage/segmentation/_felzenszwalb.py similarity index 98% rename from skimage/segmentation/felzenszwalb.py rename to skimage/segmentation/_felzenszwalb.py index cf79706d..f84f3e56 100644 --- a/skimage/segmentation/felzenszwalb.py +++ b/skimage/segmentation/_felzenszwalb.py @@ -1,7 +1,7 @@ import warnings import numpy as np -from ._felzenszwalb import _felzenszwalb_grey +from .felzenszwalb_cy import _felzenszwalb_grey def felzenszwalb(image, scale=1, sigma=0.8, min_size=20): diff --git a/skimage/segmentation/quickshift.pyx b/skimage/segmentation/_quickshift.pyx similarity index 100% rename from skimage/segmentation/quickshift.pyx rename to skimage/segmentation/_quickshift.pyx diff --git a/skimage/segmentation/slic.pyx b/skimage/segmentation/_slic.pyx similarity index 100% rename from skimage/segmentation/slic.pyx rename to skimage/segmentation/_slic.pyx diff --git a/skimage/segmentation/_felzenszwalb.pyx b/skimage/segmentation/felzenszwalb_cy.pyx similarity index 100% rename from skimage/segmentation/_felzenszwalb.pyx rename to skimage/segmentation/felzenszwalb_cy.pyx diff --git a/skimage/segmentation/setup.py b/skimage/segmentation/setup.py index 7b7d9cf1..ec092ffe 100644 --- a/skimage/segmentation/setup.py +++ b/skimage/segmentation/setup.py @@ -11,14 +11,14 @@ def configuration(parent_package='', top_path=None): config = Configuration('segmentation', parent_package, top_path) - cython(['_felzenszwalb.pyx'], working_path=base_path) - config.add_extension('_felzenszwalb', sources=['_felzenszwalb.c'], + cython(['felzenszwalb_cy.pyx'], working_path=base_path) + config.add_extension('felzenszwalb_cy', sources=['felzenszwalb_cy.c'], include_dirs=[get_numpy_include_dirs()]) - cython(['quickshift.pyx'], working_path=base_path) - config.add_extension('quickshift', sources=['quickshift.c'], + cython(['_quickshift.pyx'], working_path=base_path) + config.add_extension('_quickshift', sources=['_quickshift.c'], include_dirs=[get_numpy_include_dirs()]) - cython(['slic.pyx'], working_path=base_path) - config.add_extension('slic', sources=['slic.c'], + cython(['_slic.pyx'], working_path=base_path) + config.add_extension('_slic', sources=['_slic.c'], include_dirs=[get_numpy_include_dirs()]) return config From 75e3067acd3a7f38635b4f8d57855a0b5a6a4849 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 5 Aug 2012 21:39:03 +0100 Subject: [PATCH 207/648] DOC a bit nicer figure --- doc/examples/plot_segmentations.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py index 890ffcce..6927b323 100644 --- a/doc/examples/plot_segmentations.py +++ b/doc/examples/plot_segmentations.py @@ -77,6 +77,8 @@ print("Slic number of segments: %d" % len(np.unique(segments_slic))) print("Quickshift number of segments: %d" % len(np.unique(segments_quick))) fig, ax = plt.subplots(1, 3) +fig.set_size_inches(15, 6, forward=True) +plt.subplots_adjust(0.05, 0.05, 0.95, 0.95, 0.05, 0.05) ax[0].imshow(visualize_boundaries(img, segments_fz)) ax[0].set_title("Felzenszwalbs's method") From a05c3c6637297647bbf9745dda6f88cb9bffa70e Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 5 Aug 2012 17:44:49 -0400 Subject: [PATCH 208/648] DOC: Add note about ImageCollection slicing. Also, clean up some whitespace issues. --- skimage/io/collection.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 4a74048e..6bf91f76 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -219,7 +219,8 @@ class MultiImage(object): class ImageCollection(object): """Load and manage a collection of image files. - Note that files are always stored in alphabetical order. + Note that files are always stored in alphabetical order. Also note that + slicing returns a new ImageCollection, *not* a view into the data. Parameters ---------- @@ -286,7 +287,7 @@ class ImageCollection(object): (128, 128, 3) >>> ic = io.ImageCollection('/tmp/work/*.png:/tmp/other/*.jpg') - + """ def __init__(self, load_pattern, conserve_memory=True, load_func=None): """Load and manage a collection of images.""" @@ -330,7 +331,7 @@ class ImageCollection(object): Parameters ---------- n : int or slice - The image number to be returned, or a slice selecting the images + The image number to be returned, or a slice selecting the images and ordering to be returned in a new ImageCollection. Returns @@ -342,10 +343,10 @@ class ImageCollection(object): """ if hasattr(n, '__index__'): n = n.__index__() - + if type(n) not in [int, slice]: raise TypeError('slicing must be with an int or slice object') - + if type(n) is int: n = self._check_imgnum(n) idx = n % len(self.data) @@ -356,15 +357,15 @@ class ImageCollection(object): self._cached = n return self.data[idx] - else: - # A slice object was provided, so create a new ImageCollection - # object. Any loaded image data in the original ImageCollection - # will be copied by reference to the new object. Image data + else: + # A slice object was provided, so create a new ImageCollection + # object. Any loaded image data in the original ImageCollection + # will be copied by reference to the new object. Image data # loaded after this creation is not linked. fidx = range(len(self.files))[n] new_ic = copy(self) - new_ic._files = [self.files[i] for i in fidx] - if self.conserve_memory: + new_ic._files = [self.files[i] for i in fidx] + if self.conserve_memory: if self._cached in fidx: new_ic._cached = fidx.index(self._cached) new_ic.data = np.copy(self.data) From ea02bc6170d96e2bd5b610ae24c73dd77b400efa Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sun, 5 Aug 2012 23:11:50 +0100 Subject: [PATCH 209/648] make figure smaller again. --- doc/examples/plot_segmentations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py index 6927b323..8830cebb 100644 --- a/doc/examples/plot_segmentations.py +++ b/doc/examples/plot_segmentations.py @@ -77,7 +77,7 @@ print("Slic number of segments: %d" % len(np.unique(segments_slic))) print("Quickshift number of segments: %d" % len(np.unique(segments_quick))) fig, ax = plt.subplots(1, 3) -fig.set_size_inches(15, 6, forward=True) +fig.set_size_inches(8, 3, forward=True) plt.subplots_adjust(0.05, 0.05, 0.95, 0.95, 0.05, 0.05) ax[0].imshow(visualize_boundaries(img, segments_fz)) From eb1e71114ca161615d7bce080ae9a2ccc0bf465a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 8 Aug 2012 19:13:45 +0200 Subject: [PATCH 210/648] fix and improve estimation of geometric transformation parameters Design matrix was not composed correctly as functional model was incorrect. Additionally estimation is now based on total least-squares method. --- skimage/transform/_geometric.py | 49 ++++++++++++++++++----- skimage/transform/tests/test_geometric.py | 6 +-- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index c1c39b2b..0c767cd7 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -133,6 +133,11 @@ class ProjectiveTransform(GeometricTransform): """Set the transformation matrix with the explicit transformation parameters. + You can determine the over-, well- and under-determined parameters + with the total least-squares method. + + Number of source must match number of destination coordinates. + Parameters ---------- src : Nx2 array @@ -148,7 +153,7 @@ class ProjectiveTransform(GeometricTransform): rows = src.shape[0] #: params: a0, a1, a2, b0, b1, b2, c0, c1 - A = np.zeros((rows * 2, 8)) + A = np.zeros((rows * 2, 9)) A[:rows, 0] = xs A[:rows, 1] = ys A[:rows, 2] = 1 @@ -159,14 +164,18 @@ class ProjectiveTransform(GeometricTransform): A[rows:, 5] = 1 A[rows:, 6] = - yd * xs A[rows:, 7] = - yd * ys + A[:rows, 8] = xd + A[rows:, 8] = yd # Select relevant columns, depending on coeffs - A = A[:, self._coefs] + A = A[:, self._coefs + [8]] - b = np.hstack([xd, yd]) + _, _, V = np.linalg.svd(A) H = np.zeros((3, 3)) - H.flat[self._coefs] = np.linalg.lstsq(A, b)[0] + # solution is eigen vector that corresponds to smallest eigen value + # and normed by c3 + H.flat[self._coefs + [8]] = - V[-1, :-1] / V[-1, -1] H[2, 2] = 1 self._matrix = H @@ -294,6 +303,11 @@ class SimilarityTransform(ProjectiveTransform): """Set the transformation matrix with the explicit transformation parameters. + You can determine the over-, well- and under-determined parameters + with the total least-squares method. + + Number of source must match number of destination coordinates. + Parameters ---------- src : Nx2 array @@ -309,17 +323,20 @@ class SimilarityTransform(ProjectiveTransform): rows = src.shape[0] #: params: a0, a1, b0, b1 - A = np.zeros((rows * 2, 4)) + A = np.zeros((rows * 2, 5)) A[:rows, 0] = xs A[:rows, 2] = - ys A[:rows, 1] = 1 A[rows:, 2] = xs A[rows:, 0] = ys A[rows:, 3] = 1 + A[:rows, 4] = xd + A[rows:, 4] = yd - b = np.hstack([xd, yd]) + _, _, V = np.linalg.svd(A) + + a0, a1, b0, b1 = - V[-1, :-1] / V[-1, -1] - a0, a1, b0, b1 = np.linalg.lstsq(A, b)[0] self._matrix = np.array([[a0, -b0, a1], [b0, a0, b1], [ 0, 0, 1]]) @@ -392,6 +409,11 @@ class PolynomialTransform(GeometricTransform): """Set the transformation matrix with the explicit transformation parameters. + You can determine the over-, well- and under-determined parameters + with the total least-squares method. + + Number of source must match number of destination coordinates. + Parameters ---------- src : Nx2 array @@ -411,7 +433,7 @@ class PolynomialTransform(GeometricTransform): # number of unknown polynomial coefficients u = (order + 1) * (order + 2) - A = np.zeros((rows * 2, u)) + A = np.zeros((rows * 2, u + 1)) pidx = 0 for j in xrange(order + 1): for i in xrange(j + 1): @@ -419,9 +441,14 @@ class PolynomialTransform(GeometricTransform): A[rows:, pidx + u / 2] = xs ** (j - i) * ys ** i pidx += 1 - b = np.hstack([xd, yd]) + A[:rows, -1] = xd + A[rows:, -1] = yd - self._coeffs = np.linalg.lstsq(A, b)[0].reshape((2, u / 2)) + _, _, V = np.linalg.svd(A) + + coeffs = - V[-1, :-1] / V[-1, -1] + + self._coeffs = coeffs.reshape((2, u / 2)) def __call__(self, coords): """Apply forward transformation. @@ -473,7 +500,7 @@ def estimate_transform(ttype, src, dst, **kwargs): """Estimate 2D geometric transformation parameters. You can determine the over-, well- and under-determined parameters - with the least-squares method. + with the total least-squares method. Number of source must match number of destination coordinates. diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index f99bab7a..c7305956 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -42,7 +42,6 @@ def test_similarity_estimation(): #: exact solution tform = estimate_transform('similarity', SRC[:2, :], DST[:2, :]) assert_array_almost_equal(tform(SRC[:2, :]), DST[:2, :]) - assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) assert_equal(tform._matrix[0, 0], tform._matrix[1, 1]) assert_equal(tform._matrix[0, 1], - tform._matrix[1, 0]) @@ -68,7 +67,6 @@ def test_affine_estimation(): #: exact solution tform = estimate_transform('affine', SRC[:3, :], DST[:3, :]) assert_array_almost_equal(tform(SRC[:3, :]), DST[:3, :]) - assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) #: over-determined tform = estimate_transform('affine', SRC, DST) @@ -91,10 +89,10 @@ def test_affine_implicit(): def test_projective(): #: exact solution tform = estimate_transform('projective', SRC[:4, :], DST[:4, :]) - assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) + assert_array_almost_equal(tform(SRC[:4, :]), DST[:4, :]) #: over-determined - tform = estimate_transform('projective', SRC[:4, :], DST[:4, :]) + tform = estimate_transform('projective', SRC, DST) assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) From 54452550f1d15a2a777fe7958020d6998d419fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 8 Aug 2012 19:19:18 +0200 Subject: [PATCH 211/648] fix incorrect comment --- skimage/transform/_geometric.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 0c767cd7..32d98d1c 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -173,8 +173,8 @@ class ProjectiveTransform(GeometricTransform): _, _, V = np.linalg.svd(A) H = np.zeros((3, 3)) - # solution is eigen vector that corresponds to smallest eigen value - # and normed by c3 + # solution is right singular vector that corresponds to smallest + # singular value and normed by c3 H.flat[self._coefs + [8]] = - V[-1, :-1] / V[-1, -1] H[2, 2] = 1 @@ -335,6 +335,8 @@ class SimilarityTransform(ProjectiveTransform): _, _, V = np.linalg.svd(A) + # solution is right singular vector that corresponds to smallest + # singular value and normed by c3 a0, a1, b0, b1 = - V[-1, :-1] / V[-1, -1] self._matrix = np.array([[a0, -b0, a1], @@ -446,6 +448,8 @@ class PolynomialTransform(GeometricTransform): _, _, V = np.linalg.svd(A) + # solution is right singular vector that corresponds to smallest + # singular value and normed by c3 coeffs = - V[-1, :-1] / V[-1, -1] self._coeffs = coeffs.reshape((2, u / 2)) From 70153abb834d79fe2ac49ed3bf2f257b2b6adbbf Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 9 Aug 2012 00:15:42 -0400 Subject: [PATCH 212/648] BUG: fix import of Cython extension. --- skimage/morphology/greyreconstruct.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 7f0def38..bffdbe0d 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -19,16 +19,14 @@ def reconstruction(image, mask, selem=None, offset=None): Reconstruction requires a "seed" image and a "mask" image. The seed image gets dilated until it is constrained by the mask. The "seed" and "mask" images will be the minimum and maximum possible values of the reconstructed - image. + image, respectively. Parameters ---------- image : ndarray The seed image. - mask : ndarray The maximum allowed value at each point. - selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. @@ -84,9 +82,9 @@ def reconstruction(image, mask, selem=None, offset=None): assert tuple(image.shape) == tuple(mask.shape) assert np.all(image <= mask) try: - from ._morphrec import reconstruction_loop + from ._greyreconstruct import reconstruction_loop except ImportError: - raise ImportError("_morphrec extension not available.") + raise ImportError("_greyreconstruct extension not available.") if selem is None: selem = np.ones([3]*image.ndim, bool) From e4dd658daf138dec5b05527dc6076ee0dbe03f21 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 9 Aug 2012 00:21:59 -0400 Subject: [PATCH 213/648] STY: PEP8 and other clean up. --- skimage/morphology/greyreconstruct.py | 43 ++++++++----------- .../morphology/tests/test_reconstruction.py | 6 +-- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index bffdbe0d..8556e444 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -87,62 +87,57 @@ def reconstruction(image, mask, selem=None, offset=None): raise ImportError("_greyreconstruct extension not available.") if selem is None: - selem = np.ones([3]*image.ndim, bool) + selem = np.ones([3]*image.ndim, dtype=bool) else: selem = selem.copy() if offset == None: - assert all([d % 2 == 1 for d in selem.shape]),\ - "Footprint dimensions must all be odd" - offset = np.array([d/2 for d in selem.shape]) + if not all([d % 2 == 1 for d in selem.shape]): + ValueError("Footprint dimensions must all be odd") + offset = np.array([d / 2 for d in selem.shape]) # Cross out the center of the selem - selem[[slice(d,d+1) for d in offset]] = False - # + selem[[slice(d, d + 1) for d in offset]] = False + # Construct an array that's padded on the edges so we can ignore boundaries # The array is a dstack of the image and the mask; this lets us interleave # image and mask pixels when sorting which makes list manipulations easier - # - padding = (np.array(selem.shape)/2).astype(int) - dims = np.zeros(image.ndim+1,int) + padding = (np.array(selem.shape) / 2).astype(int) + dims = np.zeros(image.ndim + 1, dtype=int) dims[1:] = np.array(image.shape)+2*padding dims[0] = 2 inside_slices = [slice(p,-p) for p in padding] - values = np.ones(dims)*np.min(image) - values[[0]+inside_slices] = image - values[[1]+inside_slices] = mask - # + values = np.ones(dims) * np.min(image) + values[[0] + inside_slices] = image + values[[1] + inside_slices] = mask + # Create a list of strides across the array to get the neighbors # within a flattened array - # value_stride = np.array(values.strides[1:]) / values.dtype.itemsize image_stride = values.strides[0] / values.dtype.itemsize - selem_mgrid = np.mgrid[[slice(-o,d - o) - for d,o in zip(selem.shape,offset)]] - selem_offsets = selem_mgrid[:,selem].transpose() + selem_mgrid = np.mgrid[[slice(-o, d - o) + for d, o in zip(selem.shape, offset)]] + selem_offsets = selem_mgrid[:, selem].transpose() strides = np.array([np.sum(value_stride * selem_offset) for selem_offset in selem_offsets], np.int32) values = values.flatten() value_sort = np.lexsort([-values]).astype(np.int32) - # + # Make a linked list of pixels sorted by value. -1 is the list terminator. - # prev = -np.ones(len(values), np.int32) next = -np.ones(len(values), np.int32) prev[value_sort[1:]] = value_sort[:-1] next[value_sort[:-1]] = value_sort[1:] - # + # Create a rank-order value array so that the Cython inner-loop # can operate on a uniform data type - # values, value_map = rank_order(values) current = value_sort[0] reconstruction_loop(values, prev, next, strides, current, image_stride) - # + # Reshape the values array to the shape of the padded image # and return the unpadded portion of that result - # values = value_map[values[:image_stride]] - values.shape = np.array(image.shape)+2*padding + values.shape = np.array(image.shape) + 2 * padding return values[inside_slices] diff --git a/skimage/morphology/tests/test_reconstruction.py b/skimage/morphology/tests/test_reconstruction.py index 61b72033..a3461f31 100644 --- a/skimage/morphology/tests/test_reconstruction.py +++ b/skimage/morphology/tests/test_reconstruction.py @@ -26,7 +26,7 @@ def test_image_less_than_mask(): """Test reconstruction where the image is uniform and less than mask""" image = np.ones((5, 5)) mask = np.ones((5, 5)) * 2 - assert np.all(reconstruction(image,mask) == 1) + assert np.all(reconstruction(image, mask) == 1) def test_one_image_peak(): @@ -34,7 +34,7 @@ def test_one_image_peak(): image = np.ones((5, 5)) image[2, 2] = 2 mask = np.ones((5, 5)) * 3 - assert np.all(reconstruction(image,mask) == 2) + assert np.all(reconstruction(image, mask) == 2) def test_two_image_peaks(): @@ -59,7 +59,7 @@ def test_two_image_peaks(): [1, 1, 1, 1, 1, 3, 3, 3], [1, 1, 1, 1, 1, 3, 3, 3], [1, 1, 1, 1, 1, 3, 3, 3]]) - assert np.all(reconstruction(image,mask) == expected) + assert np.all(reconstruction(image, mask) == expected) def test_zero_image_one_mask(): From e1caa9d4cdc12a8f1e4c8d0747a3a51a27bffcd9 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 9 Aug 2012 00:25:26 -0400 Subject: [PATCH 214/648] Update function names that were changed since original PR. --- doc/examples/applications/plot_peak_detection_comparison.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/examples/applications/plot_peak_detection_comparison.py b/doc/examples/applications/plot_peak_detection_comparison.py index 2cf7f933..5c0c7669 100644 --- a/doc/examples/applications/plot_peak_detection_comparison.py +++ b/doc/examples/applications/plot_peak_detection_comparison.py @@ -158,7 +158,7 @@ White tophat """ selem = morph.disk(10) img_t = np.uint8(img_smooth) -opening = morph.greyscale_open(img_t, selem) +opening = morph.opening(img_t, selem) top_hat = img_t - opening imshow(opening, vmin=0, vmax=255) @@ -177,7 +177,7 @@ plt.title("Tophat with disk of r = 10") """ selem = morph.disk(5) -top_hat = morph.greyscale_white_top_hat(img_t, selem) +top_hat = morph.white_tophat(img_t, selem) imshow(top_hat) plt.title("Tophat with disk of r = 5") @@ -187,7 +187,7 @@ plt.title("Tophat with disk of r = 5") """ selem = morph.square(20) -opening = morph.greyscale_open(img_t, selem) +opening = morph.opening(img_t, selem) # scikit's top hat filter uses uint8 and doesn't check for over(under)flow. mask = opening > img_t opening[mask] = img_t[mask] From e5ed1882d34a6234159f736bdbe5970fb3bbc9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 9 Aug 2012 07:51:58 +0200 Subject: [PATCH 215/648] fix and improve comments, doc strings, variable names for consistency reasons --- skimage/transform/_geometric.py | 99 ++++++++++++++++----------------- skimage/transform/_warps.py | 2 +- 2 files changed, 48 insertions(+), 53 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 32d98d1c..2db9c7d4 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -18,7 +18,7 @@ def _stackcopy(a, b): Notes ----- - Color images are stored as an ``MxNx3`` or ``MxNx4`` arrays. + Color images are stored as an ``(M, N, 3)`` or ``(M, N, 4)`` arrays. """ if a.ndim == 3: @@ -36,12 +36,12 @@ class GeometricTransform(object): Parameters ---------- - coords : Nx2 array + coords : (N, 2) array source coordinates Returns ------- - coords : Nx2 array + coords : (N, 2) array transformed coordinates """ @@ -52,12 +52,12 @@ class GeometricTransform(object): Parameters ---------- - coords : Nx2 array + coords : (N, 2) array source coordinates Returns ------- - coords : Nx2 array + coords : (N, 2) array transformed coordinates """ @@ -78,17 +78,13 @@ class ProjectiveTransform(GeometricTransform): For each homogeneous coordinate :math:`\mathbf{x} = [x, y, 1]^T`, its target position is calculated by multiplying with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`. E.g., to rotate by theta degrees - clockwise, the matrix should be - - :: + clockwise, the matrix should be:: [[cos(theta) -sin(theta) 0] [sin(theta) cos(theta) 0] [0 0 1]] - or, to translate x by 10 and y by 20, - - :: + or, to translate x by 10 and y by 20:: [[1 0 10] [0 1 20] @@ -96,12 +92,12 @@ class ProjectiveTransform(GeometricTransform): Parameters ---------- - matrix : 3x3 array, optional + matrix : (3, 3) array, optional Homogeneous transformation matrix. """ - _coefs = range(8) + coeffs = range(8) def __init__(self, matrix=None): self._matrix = matrix @@ -136,13 +132,13 @@ class ProjectiveTransform(GeometricTransform): You can determine the over-, well- and under-determined parameters with the total least-squares method. - Number of source must match number of destination coordinates. + Number of source and destination coordinates must match. Parameters ---------- - src : Nx2 array + src : (N, 2) array source coordinates - dst : Nx2 array + dst : (N, 2) array destination coordinates """ @@ -152,7 +148,7 @@ class ProjectiveTransform(GeometricTransform): yd = dst[:, 1] rows = src.shape[0] - #: params: a0, a1, a2, b0, b1, b2, c0, c1 + # params: a0, a1, a2, b0, b1, b2, c0, c1 A = np.zeros((rows * 2, 9)) A[:rows, 0] = xs A[:rows, 1] = ys @@ -167,15 +163,15 @@ class ProjectiveTransform(GeometricTransform): A[:rows, 8] = xd A[rows:, 8] = yd - # Select relevant columns, depending on coeffs - A = A[:, self._coefs + [8]] + # Select relevant columns, depending on params + A = A[:, self.coeffs + [8]] _, _, V = np.linalg.svd(A) H = np.zeros((3, 3)) # solution is right singular vector that corresponds to smallest # singular value and normed by c3 - H.flat[self._coefs + [8]] = - V[-1, :-1] / V[-1, -1] + H.flat[self.coeffs + [8]] = - V[-1, :-1] / V[-1, -1] H[2, 2] = 1 self._matrix = H @@ -216,12 +212,12 @@ class AffineTransform(ProjectiveTransform): Parameters ---------- - matrix : 3x3 array, optional + matrix : (3, 3) array, optional Homogeneous transformation matrix. """ - _coefs = range(6) + coeffs = range(6) def compose_implicit(self, scale=None, rotation=None, shear=None, translation=None): @@ -294,25 +290,24 @@ class SimilarityTransform(ProjectiveTransform): Parameters ---------- - matrix : 3x3 array, optional + matrix : (3, 3) array, optional Homogeneous transformation matrix. """ def estimate(self, src, dst): - """Set the transformation matrix with the explicit transformation - parameters. + """Set the transformation matrix with the explicit parameters. You can determine the over-, well- and under-determined parameters with the total least-squares method. - Number of source must match number of destination coordinates. + Number of source and destination coordinates must match. Parameters ---------- - src : Nx2 array + src : (N, 2) array source coordinates - dst : Nx2 array + dst : (N, 2) array destination coordinates """ @@ -322,7 +317,7 @@ class SimilarityTransform(ProjectiveTransform): yd = dst[:, 1] rows = src.shape[0] - #: params: a0, a1, b0, b1 + # params: a0, a1, b0, b1 A = np.zeros((rows * 2, 5)) A[:rows, 0] = xs A[:rows, 2] = - ys @@ -398,14 +393,14 @@ class PolynomialTransform(GeometricTransform): Parameters ---------- - coeffs : 2xN array, optional + params : (2, N) array, optional Polynomial coefficients where `N * 2 = (order + 1) * (order + 2)`. So, - a_ji is defined in `coeffs[0, :]` and b_ji in `coeffs[1, :]`. + a_ji is defined in `params[0, :]` and b_ji in `params[1, :]`. """ - def __init__(self, coeffs=None): - self._coeffs = coeffs + def __init__(self, params=None): + self._params = params def estimate(self, src, dst, order): """Set the transformation matrix with the explicit transformation @@ -414,13 +409,13 @@ class PolynomialTransform(GeometricTransform): You can determine the over-, well- and under-determined parameters with the total least-squares method. - Number of source must match number of destination coordinates. + Number of source and destination coordinates must match. Parameters ---------- - src : Nx2 array + src : (N, 2) array source coordinates - dst : Nx2 array + dst : (N, 2) array destination coordinates order : int polynomial order (number of coefficients is order + 1) @@ -437,8 +432,8 @@ class PolynomialTransform(GeometricTransform): A = np.zeros((rows * 2, u + 1)) pidx = 0 - for j in xrange(order + 1): - for i in xrange(j + 1): + for j in range(order + 1): + for i in range(j + 1): A[:rows, pidx] = xs ** (j - i) * ys ** i A[rows:, pidx + u / 2] = xs ** (j - i) * ys ** i pidx += 1 @@ -450,36 +445,36 @@ class PolynomialTransform(GeometricTransform): # solution is right singular vector that corresponds to smallest # singular value and normed by c3 - coeffs = - V[-1, :-1] / V[-1, -1] + params = - V[-1, :-1] / V[-1, -1] - self._coeffs = coeffs.reshape((2, u / 2)) + self._params = params.reshape((2, u / 2)) def __call__(self, coords): """Apply forward transformation. Parameters ---------- - coords : Nx2 array + coords : (N, 2) array source coordinates Returns ------- - coords : Nx2 array + coords : (N, 2) array transformed coordinates """ x = coords[:, 0] y = coords[:, 1] - u = len(self._coeffs.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) pidx = 0 - for j in xrange(order + 1): - for i in xrange(j + 1): - dst[:, 0] += self._coeffs[0, pidx] * x ** (j - i) * y ** i - dst[:, 1] += self._coeffs[1, pidx] * x ** (j - i) * y ** i + 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 pidx += 1 return dst @@ -506,7 +501,7 @@ def estimate_transform(ttype, src, dst, **kwargs): You can determine the over-, well- and under-determined parameters with the total least-squares method. - Number of source must match number of destination coordinates. + Number of source and destination coordinates must match. Parameters ---------- @@ -573,14 +568,14 @@ def matrix_transform(coords, matrix): Parameters ---------- - coords : Nx2 array + coords : (N, 2) array x, y coordinates to transform - matrix : 3x3 array + matrix : (3, 3) array Homogeneous transformation matrix. Returns ------- - coords : Nx2 array + coords : (N, 2) array transformed coordinates """ @@ -596,7 +591,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, image : 2-D array Input image. inverse_map : transformation object, callable xy = f(xy, **kwargs) - Inverse coordinate map. A function that transforms a Px2 array of + Inverse coordinate map. A function that transforms a (N, 2) array of ``(x, y)`` coordinates in the *output image* into their corresponding coordinates in the *source image*. In case of a transformation object its `inverse` method will be used as transformation function. Also see diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 347b8440..f09f7944 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -152,7 +152,7 @@ def homography(image, H, output_shape=None, order=1, """ import warnings warnings.warn('the homography function is deprecated; ' - 'use the `warp` and `tform` function instead', + 'use the `warp` and `ProjectiveTransform` class instead', category=DeprecationWarning) tform = ProjectiveTransform(H) From 5085ded9943cf6c3aa52b078eebd2856ed1c5ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 9 Aug 2012 07:57:44 +0200 Subject: [PATCH 216/648] fix geometric transformation example after refactoring the module --- doc/examples/applications/plot_geometric.py | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/doc/examples/applications/plot_geometric.py b/doc/examples/applications/plot_geometric.py index 90fbce51..4e0a0cea 100644 --- a/doc/examples/applications/plot_geometric.py +++ b/doc/examples/applications/plot_geometric.py @@ -28,17 +28,17 @@ Geometric transformations can either be created using the explicit parameters """ #: create using explicit parameters -tform = tf.SimilarityTransformation() +tform = tf.SimilarityTransform() scale = 1 rotation = math.pi/2 translation = (0, 1) -tform.from_params(scale, rotation, translation) -print tform.matrix +tform.compose_implicit(scale, rotation, translation) +print tform._matrix #: create using transformation matrix -matrix = tform.matrix.copy() +matrix = tform._matrix.copy() matrix[1, 2] = 2 -tform2 = tf.SimilarityTransformation(matrix) +tform2 = tf.SimilarityTransform(matrix) """ These transformation objects can be used to forward and reverse transform @@ -46,8 +46,8 @@ coordinates between the source and destination coordinate systems: """ coord = [1, 0] -print tform2.forward(coord) -print tform2.reverse(tform.forward(coord)) +print tform2(coord) +print tform2.inverse(tform(coord)) """ Image warping @@ -57,11 +57,11 @@ Geometric transformations can also be used to warp images: """ text = data.text() -tform.from_params(1, math.pi/4, (text.shape[0] / 2, -100)) +tform.compose_implicit(1, math.pi/4, (text.shape[0] / 2, -100)) -# uses tform.reverse, alternatively use tf.warp(text, tform.reverse) +# uses tform.inverse, alternatively use tf.warp(text, tform.inverse) rotated = tf.warp(text, tform) -back_rotated = tf.warp(rotated, tform.forward) +back_rotated = tf.warp(rotated, tform) plt.figure(figsize=(8, 3)) plt.subplot(131) @@ -112,7 +112,8 @@ dst = np.array(( (300, 0) )) -tform3 = tf.estimate_transformation('projective', src, dst) +tform3 = tf.ProjectiveTransform() +tform3.estimate(src, dst) warped = tf.warp(text, tform3, output_shape=(50, 300)) plt.figure(figsize=(8, 3)) From 1bb896e48735b1e61541f99277d377128bf62ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 9 Aug 2012 08:19:37 +0200 Subject: [PATCH 217/648] remove confusing comment --- skimage/transform/_geometric.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 2db9c7d4..f5ad71f4 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -170,7 +170,7 @@ class ProjectiveTransform(GeometricTransform): H = np.zeros((3, 3)) # solution is right singular vector that corresponds to smallest - # singular value and normed by c3 + # singular value H.flat[self.coeffs + [8]] = - V[-1, :-1] / V[-1, -1] H[2, 2] = 1 @@ -331,7 +331,7 @@ class SimilarityTransform(ProjectiveTransform): _, _, V = np.linalg.svd(A) # solution is right singular vector that corresponds to smallest - # singular value and normed by c3 + # singular value a0, a1, b0, b1 = - V[-1, :-1] / V[-1, -1] self._matrix = np.array([[a0, -b0, a1], @@ -444,7 +444,7 @@ class PolynomialTransform(GeometricTransform): _, _, V = np.linalg.svd(A) # solution is right singular vector that corresponds to smallest - # singular value and normed by c3 + # singular value params = - V[-1, :-1] / V[-1, -1] self._params = params.reshape((2, u / 2)) From 76931fa61be74581910d91f936564a5bc0a8335f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 9 Aug 2012 09:21:49 +0200 Subject: [PATCH 218/648] handle composition of transformations with implicit parameters with __init__ --- skimage/transform/_geometric.py | 117 +++++++++++----------- skimage/transform/tests/test_geometric.py | 18 ++-- 2 files changed, 63 insertions(+), 72 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index f5ad71f4..d394e0cf 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -214,44 +214,41 @@ class AffineTransform(ProjectiveTransform): ---------- matrix : (3, 3) array, optional Homogeneous transformation matrix. + scale : (sx, sy) as array, list or tuple, optional + scale factors + rotation : float, optional + rotation angle in counter-clockwise direction, optional + shear : float, optional + shear angle in counter-clockwise direction + translation : (tx, ty) as array, list or tuple, optional + translation parameters """ coeffs = range(6) - def compose_implicit(self, scale=None, rotation=None, shear=None, - translation=None): - """Set the transformation matrix with the implicit transformation - parameters. + def __init__(self, matrix=None, scale=None, rotation=None, shear=None, + translation=None): + params = (scale, rotation, shear, translation) + if matrix is not None: + self._matrix = matrix + elif any(param is not None for param in params): + if scale is None: + scale = (1, 1) + if rotation is None: + rotation = 0 + if shear is None: + shear = 0 + if translation is None: + translation = (0, 0) - Parameters - ---------- - scale : (sx, sy) as array, list or tuple - scale factors - rotation : float - rotation angle in counter-clockwise direction - shear : float - shear angle in counter-clockwise direction - translation : (tx, ty) as array, list or tuple - translation parameters - - """ - if scale is None: - scale = (1, 1) - if rotation is None: - rotation = 0 - if shear is None: - shear = 0 - if translation is None: - translation = (0, 0) - - sx, sy = scale - self._matrix = 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 + sx, sy = scale + self._matrix = 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 @property def scale(self): @@ -292,9 +289,36 @@ class SimilarityTransform(ProjectiveTransform): ---------- matrix : (3, 3) array, optional Homogeneous transformation matrix. + scale : float, optional + scale factor + rotation : float, optional + rotation angle in counter-clockwise direction + translation : (tx, ty) as array, list or tuple, optional + x, y translation parameters """ + def __init__(self, matrix=None, scale=None, rotation=None, + translation=None): + params = (scale, rotation, translation) + if matrix is not None: + self._matrix = matrix + elif any(param is not None for param in params): + if scale is None: + scale = 1 + if rotation is None: + rotation = 0 + if translation is None: + translation = (0, 0) + + self._matrix = np.array([ + [math.cos(rotation), - math.sin(rotation), 0], + [math.sin(rotation), math.cos(rotation), 0], + [ 0, 0, 1] + ]) + self._matrix *= scale + self._matrix[0:2, 2] = translation + def estimate(self, src, dst): """Set the transformation matrix with the explicit parameters. @@ -338,35 +362,6 @@ class SimilarityTransform(ProjectiveTransform): [b0, a0, b1], [ 0, 0, 1]]) - def compose_implicit(self, scale=None, rotation=None, translation=None): - """Set the transformation matrix with the implicit transformation - parameters. - - Parameters - ---------- - scale : float, optional - scale factor - rotation : float, optional - rotation angle in counter-clockwise direction - translation : (tx, ty) as array, list or tuple, optional - x, y translation parameters - - """ - if scale is None: - scale = 1 - if rotation is None: - rotation = 0 - if translation is None: - translation = (0, 0) - - self._matrix = np.array([ - [math.cos(rotation), - math.sin(rotation), 0], - [math.sin(rotation), math.cos(rotation), 0], - [ 0, 0, 1] - ]) - self._matrix *= scale - self._matrix[0:2, 2] = translation - @property def scale(self): if math.cos(self.rotation) == 0: diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index c7305956..b25b1aef 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -53,11 +53,11 @@ def test_similarity_estimation(): def test_similarity_implicit(): - tform = SimilarityTransform() scale = 0.1 rotation = 1 translation = (1, 1) - tform.compose_implicit(scale, rotation, translation) + 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) @@ -74,12 +74,12 @@ def test_affine_estimation(): def test_affine_implicit(): - tform = AffineTransform() scale = (0.1, 0.13) rotation = 1 shear = 0.1 translation = (1, 1) - tform.compose_implicit(scale, rotation, shear, translation) + tform = AffineTransform(scale=scale, rotation=rotation, shear=shear, + translation=translation) assert_array_almost_equal(tform.scale, scale) assert_array_almost_equal(tform.rotation, rotation) assert_array_almost_equal(tform.shear, shear) @@ -102,13 +102,9 @@ def test_polynomial(): def test_union(): - tform1 = SimilarityTransform() - tform1.compose_implicit(scale=0.1, rotation=0.3) - tform2 = SimilarityTransform() - tform2.compose_implicit(scale=0.1, rotation=0.9) - tform3 = SimilarityTransform() - tform3.compose_implicit(scale=0.1**2, rotation=0.3+0.9) - + 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 From 8e242d0436a8e49038640a559b881bb3d1aad8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 9 Aug 2012 10:28:05 +0200 Subject: [PATCH 219/648] add mathematical description of estimation in doc strings --- skimage/transform/_geometric.py | 97 ++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 3 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index d394e0cf..cb6392bc 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -77,8 +77,13 @@ class ProjectiveTransform(GeometricTransform): For each homogeneous coordinate :math:`\mathbf{x} = [x, y, 1]^T`, its target position is calculated by multiplying with the given matrix, - :math:`H`, to give :math:`H \mathbf{x}`. E.g., to rotate by theta degrees - clockwise, the matrix should be:: + :math:`H`, to give :math:`H \mathbf{x}`:: + + [[a0 a1 a2] + [b0 b1 b2] + [c0 c1 1 ]]. + + E.g., to rotate by theta degrees clockwise, the matrix should be:: [[cos(theta) -sin(theta) 0] [sin(theta) cos(theta) 0] @@ -134,6 +139,41 @@ class ProjectiveTransform(GeometricTransform): Number of source and destination coordinates must match. + The transformation is defined as:: + + X = (a0*x + a1*y + a2) / (c0*x + c1*y + 1) + Y = (b0*x + b1*y + b2) / (c0*x + c1*y + 1) + + These equations can be transformed to the following form:: + + 0 = a0*x + a1*y + a2 - c0*x*X - c1*y*X - X + 0 = b0*x + b1*y + b2 - c0*x*Y - c1*y*Y - Y + + which exist for each set of corresponding points, so we have a set of + N * 2 equations. The coefficients appear linearly so we can write + A x = 0, where:: + + A = [[x y 1 0 0 0 -x*X -y*X -X] + [0 0 0 x y 1 -x*Y -y*Y -Y] + ... + ... + ] + x.T = [a0 a1 a2 b0 b1 b2 c0 c1 c3] + + In case of total least-squares the solutions of this homogeneous system + of equations is the right singular vector of A which corresponds to the + smallest singular value normed by the coefficient c3. + + In case of the affine transformation the coefficients c0 and c1 are 0. + Thus the system of equations is:: + + A = [[x y 1 0 0 0 -X] + [0 0 0 x y 1 -Y] + ... + ... + ] + x.T = [a0 a1 a2 b0 b1 b2 c3] + Parameters ---------- src : (N, 2) array @@ -273,7 +313,7 @@ class AffineTransform(ProjectiveTransform): class SimilarityTransform(ProjectiveTransform): """2D similarity transformation of the form:: - X = a0*x + b0*y + a1 = + X = a0*x - b0*y + a1 = = m*x*cos(rotation) + m*y*sin(rotation) + a1 Y = b0*x + a0*y + b1 = @@ -327,6 +367,31 @@ class SimilarityTransform(ProjectiveTransform): Number of source and destination coordinates must match. + The transformation is defined as:: + + X = a0*x - b0*y + a1 + Y = b0*x + a0*y + b1 + + These equations can be transformed to the following form:: + + 0 = a0*x - b0*y + a1 - X + 0 = b0*x + a0*y + b1 - Y + + which exist for each set of corresponding points, so we have a set of + N * 2 equations. The coefficients appear linearly so we can write + A x = 0, where:: + + A = [[x 1 -y 0 -X] + [y 0 x 1 -Y] + ... + ... + ] + x.T = [a0 a1 b0 b1 c3] + + In case of total least-squares the solutions of this homogeneous system + of equations is the right singular vector of A which corresponds to the + smallest singular value normed by the coefficient c3. + Parameters ---------- src : (N, 2) array @@ -406,6 +471,32 @@ class PolynomialTransform(GeometricTransform): Number of source and destination coordinates must match. + The transformation is defined as:: + + X = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) + Y = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) + + These equations can be transformed to the following form:: + + 0 = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) - X + 0 = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) - Y + + which exist for each set of corresponding points, so we have a set of + N * 2 equations. The coefficients appear linearly so we can write + A x = 0, where:: + + A = [[1 x y x**2 x*y y**2 ... 0 ... 0 -X] + [0 ... 0 1 x y x**2 x*y y**2 -Y] + ... + ... + ] + x.T = [a00 a10 a11 a20 a21 a22 ... ann + b00 b10 b11 b20 b21 b22 ... bnn c3] + + In case of total least-squares the solutions of this homogeneous system + of equations is the right singular vector of A which corresponds to the + smallest singular value normed by the coefficient c3. + Parameters ---------- src : (N, 2) array From ff1c8c4376c5a542bb3b44f0bbf707f40ef95a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 9 Aug 2012 10:30:40 +0200 Subject: [PATCH 220/648] fix typo --- skimage/transform/_geometric.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index cb6392bc..efd11a29 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -160,7 +160,7 @@ class ProjectiveTransform(GeometricTransform): ] x.T = [a0 a1 a2 b0 b1 b2 c0 c1 c3] - In case of total least-squares the solutions of this homogeneous system + In case of total least-squares the solution of this homogeneous system of equations is the right singular vector of A which corresponds to the smallest singular value normed by the coefficient c3. @@ -388,7 +388,7 @@ class SimilarityTransform(ProjectiveTransform): ] x.T = [a0 a1 b0 b1 c3] - In case of total least-squares the solutions of this homogeneous system + In case of total least-squares the solution of this homogeneous system of equations is the right singular vector of A which corresponds to the smallest singular value normed by the coefficient c3. @@ -493,7 +493,7 @@ class PolynomialTransform(GeometricTransform): x.T = [a00 a10 a11 a20 a21 a22 ... ann b00 b10 b11 b20 b21 b22 ... bnn c3] - In case of total least-squares the solutions of this homogeneous system + In case of total least-squares the solution of this homogeneous system of equations is the right singular vector of A which corresponds to the smallest singular value normed by the coefficient c3. From 9cea60d817670b7439ecba97a455c319c77f565a Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 9 Aug 2012 09:49:22 -0400 Subject: [PATCH 221/648] STY: minor PEP8 change --- skimage/morphology/greyreconstruct.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 8556e444..145d007a 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -103,9 +103,9 @@ def reconstruction(image, mask, selem=None, offset=None): # image and mask pixels when sorting which makes list manipulations easier padding = (np.array(selem.shape) / 2).astype(int) dims = np.zeros(image.ndim + 1, dtype=int) - dims[1:] = np.array(image.shape)+2*padding + dims[1:] = np.array(image.shape) + 2 * padding dims[0] = 2 - inside_slices = [slice(p,-p) for p in padding] + inside_slices = [slice(p, -p) for p in padding] values = np.ones(dims) * np.min(image) values[[0] + inside_slices] = image values[[1] + inside_slices] = mask From a3ec8e04829dec1cdc969f7817fb30d073af348d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 10 Aug 2012 07:35:16 +0200 Subject: [PATCH 222/648] change inverse_map parameter handling of warp function --- skimage/transform/_geometric.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index efd11a29..c2bd7cb3 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -679,9 +679,8 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, inverse_map : transformation object, callable xy = f(xy, **kwargs) Inverse coordinate map. A function that transforms a (N, 2) array of ``(x, y)`` coordinates in the *output image* into their corresponding - coordinates in the *source image*. In case of a transformation object - its `inverse` method will be used as transformation function. Also see - examples below. + coordinates in the *source image* (e.g. a transformation object or its + inverse). map_args : dict, optional Keyword arguments passed to `inverse_map`. output_shape : tuple (rows, cols) @@ -735,8 +734,6 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # Map each (x, y) pair to the source image according to # the user-provided mapping - if callable(getattr(inverse_map, 'inverse', None)): - inverse_map = inverse_map.inverse tf_coords = inverse_map(tf_coords, **map_args) # Reshape back to a (2, M, N) coordinate grid From 724a931d4217175a85a7aea02c77a48ec7d222f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 10 Aug 2012 07:49:04 +0200 Subject: [PATCH 223/648] adapt geometric example script to new API and improve some expressions --- doc/examples/applications/plot_geometric.py | 81 ++++++++++----------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/doc/examples/applications/plot_geometric.py b/doc/examples/applications/plot_geometric.py index 4e0a0cea..337ecad7 100644 --- a/doc/examples/applications/plot_geometric.py +++ b/doc/examples/applications/plot_geometric.py @@ -25,24 +25,27 @@ affine, projective and polynomial. Geometric transformations can either be created using the explicit parameters (e.g. scale, shear, rotation and translation) or the transformation matrix: + +First we create a transformation using explicit parameters: """ -#: create using explicit parameters -tform = tf.SimilarityTransform() -scale = 1 -rotation = math.pi/2 -translation = (0, 1) -tform.compose_implicit(scale, rotation, translation) +tform = tf.SimilarityTransform(scale=1, rotation=math.pi / 2, + translation=(0, 1)) print tform._matrix -#: create using transformation matrix +""" +Alternatively you can define a transformation by the transformation matrix +itself: +""" + matrix = tform._matrix.copy() matrix[1, 2] = 2 tform2 = tf.SimilarityTransform(matrix) """ -These transformation objects can be used to forward and reverse transform -coordinates between the source and destination coordinate systems: +These transformation objects can then be used to apply forward and inverse +coordinate transformations between the source and destination coordinate +systems: """ coord = [1, 0] @@ -57,26 +60,22 @@ Geometric transformations can also be used to warp images: """ text = data.text() -tform.compose_implicit(1, math.pi/4, (text.shape[0] / 2, -100)) -# uses tform.inverse, alternatively use tf.warp(text, tform.inverse) +tform = tf.SimilarityTransform(scale=1, rotation=math.pi / 4, + translation=(text.shape[0] / 2, -100)) + rotated = tf.warp(text, tform) -back_rotated = tf.warp(rotated, tform) +back_rotated = tf.warp(rotated, tform.inverse) -plt.figure(figsize=(8, 3)) -plt.subplot(131) -plt.imshow(text) -plt.axis('off') +fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, figsize=(8, 3)) +fig.subplots_adjust(**margins) plt.gray() -plt.subplot(132) -plt.imshow(rotated) -plt.axis('off') -plt.gray() -plt.subplot(133) -plt.imshow(back_rotated) -plt.axis('off') -plt.gray() -plt.subplots_adjust(**margins) +ax1.imshow(text) +ax1.axis('off') +ax2.imshow(rotated) +ax2.axis('off') +ax3.imshow(back_rotated) +ax3.axis('off') """ .. image:: PLOT2RST.current_figure @@ -88,7 +87,8 @@ In addition to the basic functionality mentioned above you can also estimate the parameters of a geometric transformation using the least-squares method. This can amongst other things be used for image registration or rectification, -where you have a set of control points or homologous points in two images. +where you have a set of control points or homologous/corresponding points in two +images. Let's assume we want to recognize letters on a photograph which was not taken from the front but at a certain angle. In the simplest case of a plane paper @@ -100,33 +100,30 @@ the image so that the distortion is removed and then apply a matching algorithm: text = data.text() src = np.array(( - (155, 15), - (65, 40), - (260, 130), - (360, 95) -)) -dst = np.array(( (0, 0), (0, 50), (300, 50), (300, 0) )) +dst = np.array(( + (155, 15), + (65, 40), + (260, 130), + (360, 95) +)) tform3 = tf.ProjectiveTransform() tform3.estimate(src, dst) warped = tf.warp(text, tform3, output_shape=(50, 300)) -plt.figure(figsize=(8, 3)) -plt.subplot(211) -plt.imshow(text) -plt.plot(src[:, 0], src[:, 1], '.r') -plt.axis('off') +fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(8, 3)) +fig.subplots_adjust(**margins) plt.gray() -plt.subplot(212) -plt.imshow(warped) -plt.axis('off') -plt.gray() -plt.subplots_adjust(**margins) +ax1.imshow(text) +ax1.plot(dst[:, 0], dst[:, 1], '.r') +ax1.axis('off') +ax2.imshow(warped) +ax2.axis('off') """ .. image:: PLOT2RST.current_figure From 8ab0c9a32958e7a03946473c5dc6ea8a574765f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 10 Aug 2012 08:15:06 +0200 Subject: [PATCH 224/648] add test for shape of transformation matrix --- skimage/transform/_geometric.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index c2bd7cb3..54865b02 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -105,6 +105,8 @@ class ProjectiveTransform(GeometricTransform): coeffs = range(8) def __init__(self, matrix=None): + if matrix is not None and matrix.shape != (3, 3): + raise ValueError("invalid shape of transformation matrix") self._matrix = matrix @property @@ -269,10 +271,12 @@ class AffineTransform(ProjectiveTransform): def __init__(self, matrix=None, scale=None, rotation=None, shear=None, translation=None): + if matrix is not None and matrix.shape != (3, 3): + raise ValueError("invalid shape of transformation matrix") + self._matrix = matrix + params = (scale, rotation, shear, translation) - if matrix is not None: - self._matrix = matrix - elif any(param is not None for param in params): + if any(param is not None for param in params): if scale is None: scale = (1, 1) if rotation is None: @@ -340,10 +344,12 @@ class SimilarityTransform(ProjectiveTransform): def __init__(self, matrix=None, scale=None, rotation=None, translation=None): + if matrix is not None and matrix.shape != (3, 3): + raise ValueError("invalid shape of transformation matrix") + self._matrix = matrix + params = (scale, rotation, translation) - if matrix is not None: - self._matrix = matrix - elif any(param is not None for param in params): + if any(param is not None for param in params): if scale is None: scale = 1 if rotation is None: @@ -460,6 +466,8 @@ class PolynomialTransform(GeometricTransform): """ def __init__(self, params=None): + if params is not None and params.shape[0] != 2: + raise ValueError("invalid shape of transformation parameters") self._params = params def estimate(self, src, dst, order): From c7cac1cb0ffef4d0627837a17c808ef33cff36c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 10 Aug 2012 08:16:05 +0200 Subject: [PATCH 225/648] fix, improve and extend test cases related to geometric transformations --- skimage/transform/tests/test_geometric.py | 77 ++++++++++++++++++----- skimage/transform/tests/test_warps.py | 4 +- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index b25b1aef..d3e7f20c 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -39,20 +39,26 @@ def test_stackcopy(): def test_similarity_estimation(): - #: exact solution + # 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]) - #: over-determined - tform = estimate_transform('similarity', SRC, DST) - assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) - assert_equal(tform._matrix[0, 0], tform._matrix[1, 1]) - assert_equal(tform._matrix[0, 1], - tform._matrix[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]) + + # via estimate method + tform3 = SimilarityTransform() + tform3.estimate(SRC, DST) + assert_array_almost_equal(tform3._matrix, tform2._matrix) -def test_similarity_implicit(): +def test_similarity_init(): + # init with implicit parameters scale = 0.1 rotation = 1 translation = (1, 1) @@ -62,18 +68,30 @@ def test_similarity_implicit(): assert_array_almost_equal(tform.rotation, rotation) assert_array_almost_equal(tform.translation, translation) + # init with transformation matrix + tform2 = SimilarityTransform(tform._matrix) + assert_array_almost_equal(tform2.scale, scale) + assert_array_almost_equal(tform2.rotation, rotation) + assert_array_almost_equal(tform2.translation, translation) + def test_affine_estimation(): - #: exact solution + # exact solution tform = estimate_transform('affine', SRC[:3, :], DST[:3, :]) assert_array_almost_equal(tform(SRC[:3, :]), DST[:3, :]) - #: over-determined - tform = estimate_transform('affine', SRC, DST) - assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) + # over-determined + tform2 = estimate_transform('affine', SRC, DST) + assert_array_almost_equal(tform2.inverse(tform2(SRC)), SRC) + + # via estimate method + tform3 = AffineTransform() + tform3.estimate(SRC, DST) + assert_array_almost_equal(tform3._matrix, tform2._matrix) -def test_affine_implicit(): +def test_affine_init(): + # init with implicit parameters scale = (0.1, 0.13) rotation = 1 shear = 0.1 @@ -85,21 +103,46 @@ def test_affine_implicit(): assert_array_almost_equal(tform.shear, shear) assert_array_almost_equal(tform.translation, translation) + # init with transformation matrix + tform2 = AffineTransform(tform._matrix) + assert_array_almost_equal(tform2.scale, scale) + assert_array_almost_equal(tform2.rotation, rotation) + assert_array_almost_equal(tform2.shear, shear) + assert_array_almost_equal(tform2.translation, translation) -def test_projective(): - #: exact solution + +def test_projective_estimation(): + # exact solution tform = estimate_transform('projective', SRC[:4, :], DST[:4, :]) assert_array_almost_equal(tform(SRC[:4, :]), DST[:4, :]) - #: over-determined + # over-determined + tform2 = estimate_transform('projective', SRC, DST) + assert_array_almost_equal(tform2.inverse(tform2(SRC)), SRC) + + # via estimate method + tform3 = ProjectiveTransform() + tform3.estimate(SRC, DST) + assert_array_almost_equal(tform3._matrix, tform2._matrix) + + +def test_projective_init(): tform = estimate_transform('projective', SRC, DST) - assert_array_almost_equal(tform.inverse(tform(SRC)), SRC) + # init with transformation matrix + tform2 = ProjectiveTransform(tform._matrix) + assert_array_almost_equal(tform2._matrix, tform._matrix) -def test_polynomial(): +def test_polynomial_estimation(): + # over-determined tform = estimate_transform('polynomial', SRC, DST, order=10) assert_array_almost_equal(tform(SRC), DST, 6) + # via estimate method + tform2 = PolynomialTransform() + tform2.estimate(SRC, DST, order=10) + assert_array_almost_equal(tform2._params, tform._params) + def test_union(): tform1 = SimilarityTransform(scale=0.1, rotation=0.3) diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 047b477b..7c2e52f2 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -11,8 +11,8 @@ def test_warp(): x = np.zeros((5, 5), dtype=np.uint8) x[2, 2] = 255 x = img_as_float(x) - theta = -np.pi/2 - tform = SimilarityTransform(1, theta, (0, 4)) + theta = - np.pi / 2 + tform = SimilarityTransform(scale=1, rotation=theta, translation=(0, 4)) x90 = warp(x, tform, order=1) assert_array_almost_equal(x90, np.rot90(x)) From 08f4379e0e38fb3cd7bae927a3d5a32185636f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 10 Aug 2012 08:19:48 +0200 Subject: [PATCH 226/648] add information about unit of angles to doc strings --- skimage/transform/_geometric.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 54865b02..19e3cb2d 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -259,9 +259,9 @@ class AffineTransform(ProjectiveTransform): scale : (sx, sy) as array, list or tuple, optional scale factors rotation : float, optional - rotation angle in counter-clockwise direction, optional + rotation angle in counter-clockwise direction as radians shear : float, optional - shear angle in counter-clockwise direction + shear angle in counter-clockwise direction as radians translation : (tx, ty) as array, list or tuple, optional translation parameters @@ -334,9 +334,9 @@ class SimilarityTransform(ProjectiveTransform): matrix : (3, 3) array, optional Homogeneous transformation matrix. scale : float, optional - scale factor + scale factor rotation : float, optional - rotation angle in counter-clockwise direction + rotation angle in counter-clockwise direction as radians translation : (tx, ty) as array, list or tuple, optional x, y translation parameters From 6ead3097c2fa262e396f27ed1ae7934a149a4719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 10 Aug 2012 09:10:06 +0200 Subject: [PATCH 227/648] cast input of image label function --- skimage/morphology/ccomp.pyx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/morphology/ccomp.pyx b/skimage/morphology/ccomp.pyx index a1d5f303..6e5f2297 100644 --- a/skimage/morphology/ccomp.pyx +++ b/skimage/morphology/ccomp.pyx @@ -77,8 +77,7 @@ cdef link_bg(np.int_t *forest, np.int_t n, np.int_t *background_node): # Connected components search as described in Fiorio et al. -def label(np.ndarray[DTYPE_t, ndim=2] input, - np.int_t neighbors=8, np.int_t background=-1): +def label(input, np.int_t neighbors=8, np.int_t background=-1): """Label connected regions of an integer array. Two pixels are connected when they are neighbors and have the same value. @@ -89,7 +88,7 @@ def label(np.ndarray[DTYPE_t, ndim=2] input, [ ] [ ] [ ] [ ] | \ | / [ ]--[ ]--[ ] [ ]--[ ]--[ ] - | / | \ + | / | \ [ ] [ ] [ ] [ ] Parameters @@ -139,7 +138,8 @@ def label(np.ndarray[DTYPE_t, ndim=2] input, cdef np.int_t rows = input.shape[0] cdef np.int_t cols = input.shape[1] - cdef np.ndarray[DTYPE_t, ndim=2] data = input.copy() + cdef np.ndarray[DTYPE_t, ndim=2] data = np.array(input, copy=True, + dtype=DTYPE) cdef np.ndarray[DTYPE_t, ndim=2] forest forest = np.arange(data.size, dtype=DTYPE).reshape((rows, cols)) From f88a29b091c6af19e7d3df3bc8f91c49216106e5 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 10 Aug 2012 10:03:20 +0100 Subject: [PATCH 228/648] ENH minor speedups. --- skimage/segmentation/_slic.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 98a6f6bc..684740d6 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -51,9 +51,10 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, image = rgb2lab(image) # initialize on grid: + cdef int height, width height, width = image.shape[:2] # approximate grid size for desired n_segments - step = np.ceil(np.sqrt(height * width / n_segments)) + cdef int step = np.ceil(np.sqrt(height * width / n_segments)) grid_y, grid_x = np.mgrid[:height, :width] means_y = grid_y[::step, ::step] means_x = grid_x[::step, ::step] From 8f5337a2bf82267b14b01630f3e513042b1d8082 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 10 Aug 2012 10:31:23 +0100 Subject: [PATCH 229/648] ENH added some cdefs --- skimage/segmentation/_quickshift.pyx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/_quickshift.pyx b/skimage/segmentation/_quickshift.pyx index dc4d86e1..c050688e 100644 --- a/skimage/segmentation/_quickshift.pyx +++ b/skimage/segmentation/_quickshift.pyx @@ -16,7 +16,7 @@ cdef extern from "math.h": @cython.boundscheck(False) @cython.wraparound(False) @cython.cdivision(True) -def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, +def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=False, sigma=0, convert2lab=True, random_seed=None): """Segments image using quickshift clustering in Color-(x,y) space. @@ -94,7 +94,8 @@ def quickshift(image, ratio=1., kernel_size=5, max_dist=10, return_tree=False, cdef int height = image_c.shape[0] cdef int width = image_c.shape[1] cdef int channels = image_c.shape[2] - cdef float closest, dist + cdef double current_density, closest, dist + cdef int x, y, x_, y_ cdef np.float_t* image_p = image_c.data From 312b03d1b141bf6e805544322bcae3f9e34bac86 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 10 Aug 2012 10:35:23 +0100 Subject: [PATCH 230/648] ENH more speeeeed --- skimage/segmentation/_quickshift.pyx | 57 +++++++++++++++------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/skimage/segmentation/_quickshift.pyx b/skimage/segmentation/_quickshift.pyx index c050688e..4267c17b 100644 --- a/skimage/segmentation/_quickshift.pyx +++ b/skimage/segmentation/_quickshift.pyx @@ -11,6 +11,7 @@ from ..color import rgb2lab cdef extern from "math.h": double exp(double) + double sqrt(double) @cython.boundscheck(False) @@ -104,16 +105,18 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=Fa cdef np.ndarray[dtype=np.float_t, ndim=2] densities \ = np.zeros((height, width)) # compute densities - for x, y in product(xrange(height), xrange(width)): - x_min, x_max = max(x - w, 0), min(x + w + 1, height) - y_min, y_max = max(y - w, 0), min(y + w + 1, width) - for x_, y_ in product(xrange(x_min, x_max), xrange(y_min, y_max)): - dist = 0 - for c in xrange(channels): - dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 - dist += (x - x_)**2 + (y - y_)**2 - densities[x, y] += exp(-dist / (2 * kernel_size**2)) - current_pixel_p += channels + for x in range(height): + for y in range(width): + x_min, x_max = max(x - w, 0), min(x + w + 1, height) + y_min, y_max = max(y - w, 0), min(y + w + 1, width) + for x_ in range(x_min, x_max): + for y_ in range(y_min, y_max): + dist = 0 + for c in range(channels): + dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 + dist += (x - x_)**2 + (y - y_)**2 + densities[x, y] += exp(-dist / (2 * kernel_size**2)) + current_pixel_p += channels # this will break ties that otherwise would give us headache densities += random_state.normal(scale=0.00001, size=(height, width)) @@ -125,22 +128,24 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=Fa = np.zeros((height, width)) # find nearest node with higher density current_pixel_p = image_p - for x, y in product(xrange(height), xrange(width)): - current_density = densities[x, y] - closest = np.inf - x_min, x_max = max(x - w, 0), min(x + w + 1, height) - y_min, y_max = max(y - w, 0), min(y + w + 1, width) - for x_, y_ in product(xrange(x_min, x_max), xrange(y_min, y_max)): - if densities[x_, y_] > current_density: - dist = 0 - for c in xrange(channels): - dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 - dist += (x - x_)**2 + (y - y_)**2 - if dist < closest: - closest = dist - parent[x, y] = x_ * width + y_ - dist_parent[x, y] = np.sqrt(closest) - current_pixel_p += channels + for x in range(height): + for y in range(width): + current_density = densities[x, y] + closest = np.inf + x_min, x_max = max(x - w, 0), min(x + w + 1, height) + y_min, y_max = max(y - w, 0), min(y + w + 1, width) + for x_ in range(x_min, x_max): + for y_ in range(y_min, y_max): + if densities[x_, y_] > current_density: + dist = 0 + for c in range(channels): + dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 + dist += (x - x_)**2 + (y - y_)**2 + if dist < closest: + closest = dist + parent[x, y] = x_ * width + y_ + dist_parent[x, y] = sqrt(closest) + current_pixel_p += channels dist_parent_flat = dist_parent.ravel() flat = parent.ravel() From 8d769a4cd966a29ad0764add3a73b4c074c003f6 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 10 Aug 2012 10:48:38 +0100 Subject: [PATCH 231/648] ENH Felzenszwalbs segmentation somewhat faster --- skimage/segmentation/felzenszwalb_cy.pyx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/skimage/segmentation/felzenszwalb_cy.pyx b/skimage/segmentation/felzenszwalb_cy.pyx index 76320d3a..c5f3e705 100644 --- a/skimage/segmentation/felzenszwalb_cy.pyx +++ b/skimage/segmentation/felzenszwalb_cy.pyx @@ -1,13 +1,16 @@ import numpy as np cimport numpy as np import scipy +cimport cython from skimage.morphology.ccomp cimport find_root, join_trees from ..util import img_as_float - -def _felzenszwalb_grey(image, scale=1, sigma=0.8, min_size=20): +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) +def _felzenszwalb_grey(image, double scale=1, sigma=0.8, int min_size=20): """Felzenszwalb's efficient graph based segmentation for a single channel. Produces an oversegmentation of a 2d image using a fast, minimum spanning @@ -70,7 +73,7 @@ def _felzenszwalb_grey(image, scale=1, sigma=0.8, min_size=20): = np.ones(width * height, dtype=np.int) # inner cost of segments cdef np.ndarray[np.float_t, ndim=1] cint = np.zeros(width * height) - cdef int seg0, seg1, seg_new + cdef int seg0, seg1, seg_new, e cdef float cost, inner_cost0, inner_cost1 # set costs_p back one. we increase it before we use it # since we might continue before that. From a87779b650e4fa1e6abd08510d2dc241e6fd9dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 10 Aug 2012 20:45:50 +0200 Subject: [PATCH 232/648] fix num_peaks parameter bug in peak_local_max --- skimage/feature/peak.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/feature/peak.py b/skimage/feature/peak.py index d8e54dd6..57eb3bb7 100644 --- a/skimage/feature/peak.py +++ b/skimage/feature/peak.py @@ -95,9 +95,9 @@ def peak_local_max(image, min_distance=10, threshold='deprecated', # get coordinates of peaks coordinates = np.transpose(image_t.nonzero()) - if len(coordinates) > num_peaks: - intensities = image[tuple(coordinates.T)] + if coordinates.shape[0] > num_peaks: + intensities = image[coordinates[:, 0], coordinates[:, 1]] idx_maxsort = np.argsort(intensities)[::-1] - coordinates = coordinates[idx_maxsort][:2] + coordinates = coordinates[idx_maxsort][:num_peaks] return coordinates From 4541146561d43ce2ad4ce9c3236ff60241e2b5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 12 Aug 2012 10:13:29 +0200 Subject: [PATCH 233/648] raise exception if matrix and implicit parameter arguments provided --- skimage/transform/_geometric.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 19e3cb2d..13bcc7f6 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -271,12 +271,16 @@ class AffineTransform(ProjectiveTransform): def __init__(self, matrix=None, scale=None, rotation=None, shear=None, translation=None): - if matrix is not None and matrix.shape != (3, 3): - raise ValueError("invalid shape of transformation matrix") self._matrix = matrix + params = any(param is not None + for param in (scale, rotation, shear, translation)) - params = (scale, rotation, shear, translation) - if any(param is not None for param in params): + if params and matrix is not None: + raise ValueError("You cannot specify the transformation matrix and " + "the implicit parameters at the same time.") + elif matrix is not None and matrix.shape != (3, 3): + raise ValueError("Invalid shape of transformation matrix.") + elif params: if scale is None: scale = (1, 1) if rotation is None: @@ -344,12 +348,16 @@ class SimilarityTransform(ProjectiveTransform): def __init__(self, matrix=None, scale=None, rotation=None, translation=None): - if matrix is not None and matrix.shape != (3, 3): - raise ValueError("invalid shape of transformation matrix") self._matrix = matrix + params = any(param is not None + for param in (scale, rotation, translation)) - params = (scale, rotation, translation) - if any(param is not None for param in params): + if params and matrix is not None: + raise ValueError("You cannot specify the transformation matrix and " + "the implicit parameters at the same time.") + elif matrix is not None and matrix.shape != (3, 3): + raise ValueError("Invalid shape of transformation matrix.") + elif params: if scale is None: scale = 1 if rotation is None: From 9bd445700a059b5b861f10d7ff78a2cf0bd0ed75 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Sun, 12 Aug 2012 18:20:58 -0500 Subject: [PATCH 234/648] add note about to display protocol to Image docstring --- skimage/io/_io.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 95cb5abc..20337797 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -16,7 +16,11 @@ _image_stack = [] class Image(np.ndarray): - """Image data with tags.""" + """Class representing Image data. + + These objects have tags for image metadata and IPython display protocol + methods for image display. + """ tags = {'filename': '', 'EXIF': {}, From fabedb58ec4bcdb24dd816c3a405779adff7cf22 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Sun, 12 Aug 2012 18:54:43 -0500 Subject: [PATCH 235/648] add unit test for Image._repr_png_() with PIL plugin --- skimage/io/tests/test_pil.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index e442c856..fa62e1dd 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -33,6 +33,7 @@ def setup_module(self): pass + @skipif(not PIL_available) def test_imread_flatten(): # a color image is flattened @@ -78,6 +79,20 @@ def test_imread_uint16(): assert_array_almost_equal(img, expected) +@skipif(not PIL_available) +def test_repr_png(): + img_path = os.path.join(data_dir, 'camera.png') + original_img = imread(img_path) + original_img_str = original_img._repr_png_() + + with NamedTemporaryFile(suffix='.png', mode='r+') as temp_png: + temp_png.write(original_img_str) + temp_png.seek(0) + round_trip = imread(temp_png) + + assert np.all(original_img == round_trip) + + # Big endian images not correctly loaded for PIL < 1.1.7 # Renable test when PIL 1.1.7 is more common. @skipif(True) From b1139c724d4cd957883ebbd31d0497bb81e8c25a Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 13 Aug 2012 12:02:32 -0700 Subject: [PATCH 236/648] DOC: Fix typos in see-also references. --- skimage/io/collection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 6bf91f76..37eacfff 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -26,7 +26,7 @@ def concatenate_images(ic): See Also -------- - `ImageCollection.concatenate`, `MultiImage.concatenate` + ImageCollection.concatenate, MultiImage.concatenate Raises ------ @@ -207,7 +207,7 @@ class MultiImage(object): See Also -------- - `concatenate_images` + concatenate_images Raises ------ @@ -419,7 +419,7 @@ class ImageCollection(object): See Also -------- - `concatenate_images` + concatenate_images Raises ------ From bb27e3771053c05e67b4045b7b91a78b6419afbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 14 Aug 2012 07:59:54 +0200 Subject: [PATCH 237/648] extend test cases for num_peaks parameter of peak_local_max --- skimage/feature/tests/test_peak.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/skimage/feature/tests/test_peak.py b/skimage/feature/tests/test_peak.py index eaedc84f..13457781 100644 --- a/skimage/feature/tests/test_peak.py +++ b/skimage/feature/tests/test_peak.py @@ -51,15 +51,23 @@ def test_flat_peak(): def test_num_peaks(): - image = np.zeros((3, 7), dtype=np.uint8) + image = np.zeros((7, 7), dtype=np.uint8) image[1, 1] = 10 image[1, 3] = 11 image[1, 5] = 12 - assert len(peak.peak_local_max(image, min_distance=1)) == 3 + image[3, 5] = 8 + image[5, 3] = 7 + assert len(peak.peak_local_max(image, min_distance=1)) == 5 peaks_limited = peak.peak_local_max(image, min_distance=1, num_peaks=2) assert len(peaks_limited) == 2 assert (1, 3) in peaks_limited assert (1, 5) in peaks_limited + peaks_limited = peak.peak_local_max(image, min_distance=1, num_peaks=4) + assert len(peaks_limited) == 4 + assert (1, 3) in peaks_limited + assert (1, 5) in peaks_limited + assert (1, 1) in peaks_limited + assert (3, 5) in peaks_limited if __name__ == '__main__': From 31ba6a59b40c8eaa2f5ee695883229cbff418dab Mon Sep 17 00:00:00 2001 From: Tomas Kazmar Date: Thu, 16 Aug 2012 14:05:03 +0200 Subject: [PATCH 238/648] BUG: Fix Orientation for diagonal regions in regionprops. --- skimage/measure/_regionprops.py | 7 +++++-- skimage/measure/tests/test_regionprops.py | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index ee58868c..be10d6ec 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -124,7 +124,7 @@ def regionprops(label_image, properties=['Area', 'Centroid'], * Orientation : float Angle between the X-axis and the major axis of the ellipse that has the same second-moments as the region. Ranging from `-pi/2` to - `-pi/2` in counter-clockwise direction. + `pi/2` in counter-clockwise direction. * Perimeter : float Perimeter of object which approximates the contour as a line through the centers of border pixels using a 4-connectivity. @@ -299,7 +299,10 @@ def regionprops(label_image, properties=['Area', 'Centroid'], if 'Orientation' in properties: if a - c == 0: - obj_props['Orientation'] = PI / 2 + if b > 0: + obj_props['Orientation'] = -PI / 4. + else: + obj_props['Orientation'] = PI / 4. else: obj_props['Orientation'] = - 0.5 * atan2(2 * b, (a - c)) diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index 915087dc..7faf0bac 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -197,6 +197,15 @@ def test_orientation(): # test correct quadrant determination orientation2 = regionprops(SAMPLE.T, ['Orientation'])[0]['Orientation'] assert_almost_equal(orientation2, math.pi / 2 - orientation) + # test diagonal regions + orientation_diag = regionprops(np.eye(10, dtype=int), ['Orientation'])[0]['Orientation'] + assert_almost_equal(orientation_diag, -math.pi / 4) + orientation_diag = regionprops(np.flipud(np.eye(10, dtype=int)), ['Orientation'])[0]['Orientation'] + assert_almost_equal(orientation_diag, math.pi / 4) + orientation_diag = regionprops(np.fliplr(np.eye(10, dtype=int)), ['Orientation'])[0]['Orientation'] + assert_almost_equal(orientation_diag, math.pi / 4) + orientation_diag = regionprops(np.fliplr(np.flipud(np.eye(10, dtype=int))), ['Orientation'])[0]['Orientation'] + assert_almost_equal(orientation_diag, -math.pi / 4) def test_perimeter(): perimeter = regionprops(SAMPLE, ['Perimeter'])[0]['Perimeter'] From 5efb3d41c2dc88343e804ff3c2662df81aca70cd Mon Sep 17 00:00:00 2001 From: Tomas Kazmar Date: Thu, 16 Aug 2012 16:16:56 +0200 Subject: [PATCH 239/648] Wrap long lines. --- skimage/measure/tests/test_regionprops.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index 7faf0bac..573c852a 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -198,13 +198,17 @@ def test_orientation(): orientation2 = regionprops(SAMPLE.T, ['Orientation'])[0]['Orientation'] assert_almost_equal(orientation2, math.pi / 2 - orientation) # test diagonal regions - orientation_diag = regionprops(np.eye(10, dtype=int), ['Orientation'])[0]['Orientation'] + diag = np.eye(10, dtype=int) + orientation_diag = regionprops(diag, ['Orientation'])[0]['Orientation'] assert_almost_equal(orientation_diag, -math.pi / 4) - orientation_diag = regionprops(np.flipud(np.eye(10, dtype=int)), ['Orientation'])[0]['Orientation'] + orientation_diag = regionprops(np.flipud(diag), ['Orientation'] + )[0]['Orientation'] assert_almost_equal(orientation_diag, math.pi / 4) - orientation_diag = regionprops(np.fliplr(np.eye(10, dtype=int)), ['Orientation'])[0]['Orientation'] + orientation_diag = regionprops(np.fliplr(diag), ['Orientation'] + )[0]['Orientation'] assert_almost_equal(orientation_diag, math.pi / 4) - orientation_diag = regionprops(np.fliplr(np.flipud(np.eye(10, dtype=int))), ['Orientation'])[0]['Orientation'] + orientation_diag = regionprops(np.fliplr(np.flipud(diag)), ['Orientation'] + )[0]['Orientation'] assert_almost_equal(orientation_diag, -math.pi / 4) def test_perimeter(): From 1ea41173b821124e2836e6731ff3d32f98c6e542 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 16 Aug 2012 22:42:27 -0400 Subject: [PATCH 240/648] BUG: Rename files with similar function, python module, and cython extension names. --- skimage/feature/__init__.py | 2 +- skimage/feature/{greycomatrix.py => _greycomatrix.py} | 2 +- skimage/feature/{_greycomatrix.pyx => _greycomatrix_cy.pyx} | 0 skimage/feature/setup.py | 4 ++-- skimage/morphology/__init__.py | 2 +- skimage/morphology/{skeletonize.py => _skeletonize.py} | 2 +- skimage/morphology/{_skeletonize.pyx => _skeletonize_cy.pyx} | 0 skimage/morphology/setup.py | 4 ++-- 8 files changed, 8 insertions(+), 8 deletions(-) rename skimage/feature/{greycomatrix.py => _greycomatrix.py} (99%) rename skimage/feature/{_greycomatrix.pyx => _greycomatrix_cy.pyx} (100%) rename skimage/morphology/{skeletonize.py => _skeletonize.py} (99%) rename skimage/morphology/{_skeletonize.pyx => _skeletonize_cy.pyx} (100%) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index c067268a..eb98945c 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -1,5 +1,5 @@ from ._hog import hog -from .greycomatrix import greycomatrix, greycoprops +from ._greycomatrix import greycomatrix, greycoprops from .peak import peak_local_max from ._harris import harris from .template import match_template diff --git a/skimage/feature/greycomatrix.py b/skimage/feature/_greycomatrix.py similarity index 99% rename from skimage/feature/greycomatrix.py rename to skimage/feature/_greycomatrix.py index 5b2b92db..0926117b 100644 --- a/skimage/feature/greycomatrix.py +++ b/skimage/feature/_greycomatrix.py @@ -5,7 +5,7 @@ properties to characterize image textures. import numpy as np -from ._greycomatrix import _glcm_loop +from ._greycomatrix_cy import _glcm_loop def greycomatrix(image, distances, angles, levels=256, symmetric=False, diff --git a/skimage/feature/_greycomatrix.pyx b/skimage/feature/_greycomatrix_cy.pyx similarity index 100% rename from skimage/feature/_greycomatrix.pyx rename to skimage/feature/_greycomatrix_cy.pyx diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index 39358fd3..2c50a592 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -12,10 +12,10 @@ def configuration(parent_package='', top_path=None): config = Configuration('feature', parent_package, top_path) config.add_data_dir('tests') - cython(['_greycomatrix.pyx'], working_path=base_path) + cython(['_greycomatrix_cy.pyx'], working_path=base_path) cython(['_template.pyx'], working_path=base_path) - config.add_extension('_greycomatrix', sources=['_greycomatrix.c'], + config.add_extension('_greycomatrix_cy', sources=['_greycomatrix_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_template', sources=['_template.c'], include_dirs=[get_numpy_include_dirs()]) diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index d639be1c..abc9986f 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -2,5 +2,5 @@ from .grey import * from .selem import * from .ccomp import label from .watershed import watershed, is_local_maximum -from .skeletonize import skeletonize, medial_axis +from ._skeletonize import skeletonize, medial_axis from .convex_hull import convex_hull_image diff --git a/skimage/morphology/skeletonize.py b/skimage/morphology/_skeletonize.py similarity index 99% rename from skimage/morphology/skeletonize.py rename to skimage/morphology/_skeletonize.py index a8d31bdd..58842c6d 100644 --- a/skimage/morphology/skeletonize.py +++ b/skimage/morphology/_skeletonize.py @@ -5,7 +5,7 @@ Algorithms for computing the skeleton of a binary image import numpy as np from scipy import ndimage -from ._skeletonize import _skeletonize_loop, _table_lookup_index +from ._skeletonize_cy import _skeletonize_loop, _table_lookup_index # --------- Skeletonization by morphological thinning --------- diff --git a/skimage/morphology/_skeletonize.pyx b/skimage/morphology/_skeletonize_cy.pyx similarity index 100% rename from skimage/morphology/_skeletonize.pyx rename to skimage/morphology/_skeletonize_cy.pyx diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index fcf33f7f..2dd4a378 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -15,7 +15,7 @@ def configuration(parent_package='', top_path=None): cython(['ccomp.pyx'], working_path=base_path) cython(['cmorph.pyx'], working_path=base_path) cython(['_watershed.pyx'], working_path=base_path) - cython(['_skeletonize.pyx'], working_path=base_path) + cython(['_skeletonize_cy.pyx'], working_path=base_path) cython(['_pnpoly.pyx'], working_path=base_path) cython(['_convex_hull.pyx'], working_path=base_path) @@ -25,7 +25,7 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('_watershed', sources=['_watershed.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_skeletonize', sources=['_skeletonize.c'], + config.add_extension('_skeletonize_cy', sources=['_skeletonize_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_pnpoly', sources=['_pnpoly.c'], include_dirs=[get_numpy_include_dirs()]) From e6d03eaebc54baf0f61ba65fdeb0c43f7fe1b379 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 17 Aug 2012 00:42:56 -0400 Subject: [PATCH 241/648] STY: Use standard skimage data type conversion. --- .../plot_peak_detection_comparison.py | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/doc/examples/applications/plot_peak_detection_comparison.py b/doc/examples/applications/plot_peak_detection_comparison.py index 5c0c7669..8fe26c02 100644 --- a/doc/examples/applications/plot_peak_detection_comparison.py +++ b/doc/examples/applications/plot_peak_detection_comparison.py @@ -84,23 +84,36 @@ peaks on the left to go undetected. Morphological reconstruction ============================ + +Morphological reconstruction uses two images: a seed image and a mask image. +Initially, all values of the reconstructed image start off pixel at the values +of the seed image. A seed pixels of high intensity spread outwards until it +hits the mask image (i.e. the mask value for a pixel is lower than the +high-intensity value). Note that the mask is a gray-scale image that limits the +maximum intensity at a pixel. This algorithm is clearer with pictures, which +we generate below. + +One common case uses mask and seed images derived from the same image but +shifted in intensity. Note: be careful when shifting images integer values, +since this can lead to under/overflow of values. To prevent the uint8 image we +started with from underflowing during subtraction, we first convert to float: """ -import numpy as np -img_r = np.int32(img_smooth) +from skimage import img_as_float +img_r = img_as_float(img_smooth) import skimage.morphology as morph -h = 20 +h = 0.1 rec = morph.reconstruction(img_r-h, img_r) -imshow(img_r, vmin=0, vmax=255) +imshow(img_r, vmin=0, vmax=1) plt.title("original (smoothed) image") """ .. image:: PLOT2RST.current_figure """ -imshow(rec, vmin=0, vmax=255) +imshow(rec, vmin=0, vmax=1) plt.title("background image (reconstruction)") """ @@ -156,10 +169,10 @@ features. White tophat ============ """ + selem = morph.disk(10) -img_t = np.uint8(img_smooth) -opening = morph.opening(img_t, selem) -top_hat = img_t - opening +opening = morph.opening(img_smooth, selem) +top_hat = img_smooth - opening imshow(opening, vmin=0, vmax=255) plt.title("Greyscale opening of image") @@ -177,7 +190,7 @@ plt.title("Tophat with disk of r = 10") """ selem = morph.disk(5) -top_hat = morph.white_tophat(img_t, selem) +top_hat = morph.white_tophat(img_smooth, selem) imshow(top_hat) plt.title("Tophat with disk of r = 5") @@ -187,11 +200,11 @@ plt.title("Tophat with disk of r = 5") """ selem = morph.square(20) -opening = morph.opening(img_t, selem) +opening = morph.opening(img_smooth, selem) # scikit's top hat filter uses uint8 and doesn't check for over(under)flow. -mask = opening > img_t -opening[mask] = img_t[mask] -top_hat = img_t - opening +mask = opening > img_smooth +opening[mask] = img_smooth[mask] +top_hat = img_smooth - opening imshow(opening, vmin=0, vmax=255) plt.title("Greyscale opening of image") From d0870c6d683066be169fb53f831784c50d36e686 Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Fri, 17 Aug 2012 17:55:26 -0400 Subject: [PATCH 242/648] ENH: initialize AffineTransform to identity when no args are provided --- skimage/transform/_geometric.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 13bcc7f6..51ff9519 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -297,6 +297,11 @@ class AffineTransform(ProjectiveTransform): [ 0, 0, 1] ]) self._matrix[0:2, 2] = translation + else: + # -- Default to an identity transform + self._matrix = np.asarray( + [[1, 0, 0], [0, 1, 0], [0, 0, 0]], + dtype='float64') @property def scale(self): From 33a19a8c7bb8560af53cc7b0d91e675862c6a8ca Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 17 Aug 2012 22:48:02 -0400 Subject: [PATCH 243/648] DOC: add comments to clarify algorithm --- skimage/morphology/greyreconstruct.py | 39 ++++++++++++++++----------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 145d007a..8c12a9c5 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -1,5 +1,6 @@ """ -`reconstruction` originally part of CellProfiler, code licensed under both GPL and BSD licenses. +`reconstruction` originally part of CellProfiler, code licensed under both GPL +and BSD licenses. Website: http://www.cellprofiler.org Copyright (c) 2003-2009 Massachusetts Institute of Technology @@ -14,17 +15,18 @@ from skimage.filter.rank_order import rank_order def reconstruction(image, mask, selem=None, offset=None): - """Perform a morphological reconstruction of the image. + """Perform a morphological reconstruction of an image. - Reconstruction requires a "seed" image and a "mask" image. The seed image - gets dilated until it is constrained by the mask. The "seed" and "mask" + Reconstruction requires a "seed" image and a "mask" image. Currently, this + only implements reconstruction by dilation, such that the seed image is + dilated until it is constrained by the mask. Thus, he "seed" and "mask" images will be the minimum and maximum possible values of the reconstructed image, respectively. Parameters ---------- image : ndarray - The seed image. + The seed image; a.k.a. marker image. mask : ndarray The maximum allowed value at each point. selem : ndarray @@ -42,9 +44,13 @@ def reconstruction(image, mask, selem=None, offset=None): Pattern Recognition Letters 25 (2004) 1759-1767. Applications for greyscale reconstruction are discussed in: - Vincent, L., "Morphological Grayscale Reconstruction in Image Analysis: - Applications and Efficient Algorithms", IEEE Transactions on Image - Processing (1993) + + [1] Vincent, L., "Morphological Grayscale Reconstruction in Image Analysis: + Applications and Efficient Algorithms", IEEE Transactions on Image + Processing (1993) + + [2] Soille, P., "Morphological Image Analysis: Principles and Applications", + Chapter 6, 2nd edition (2003), ISBN 3540429883. Examples -------- @@ -98,27 +104,27 @@ def reconstruction(image, mask, selem=None, offset=None): # Cross out the center of the selem selem[[slice(d, d + 1) for d in offset]] = False - # Construct an array that's padded on the edges so we can ignore boundaries - # The array is a dstack of the image and the mask; this lets us interleave - # image and mask pixels when sorting which makes list manipulations easier + # Make padding for edges of reconstructed image so we can ignore boundaries padding = (np.array(selem.shape) / 2).astype(int) dims = np.zeros(image.ndim + 1, dtype=int) dims[1:] = np.array(image.shape) + 2 * padding dims[0] = 2 inside_slices = [slice(p, -p) for p in padding] + # Set padded region to minimum image intensity and mask along first axis so + # we can interleave image and mask pixels when sorting. values = np.ones(dims) * np.min(image) values[[0] + inside_slices] = image values[[1] + inside_slices] = mask - # Create a list of strides across the array to get the neighbors - # within a flattened array + # Create a list of strides across the array to get the neighbors within + # a flattened array value_stride = np.array(values.strides[1:]) / values.dtype.itemsize image_stride = values.strides[0] / values.dtype.itemsize selem_mgrid = np.mgrid[[slice(-o, d - o) for d, o in zip(selem.shape, offset)]] selem_offsets = selem_mgrid[:, selem].transpose() - strides = np.array([np.sum(value_stride * selem_offset) - for selem_offset in selem_offsets], np.int32) + nb_strides = np.array([np.sum(value_stride * selem_offset) + for selem_offset in selem_offsets], np.int32) values = values.flatten() value_sort = np.lexsort([-values]).astype(np.int32) @@ -130,10 +136,11 @@ def reconstruction(image, mask, selem=None, offset=None): # Create a rank-order value array so that the Cython inner-loop # can operate on a uniform data type + # fragile: `reconstruction_loop` needs 'uint32' conversion by `rank_order` values, value_map = rank_order(values) current = value_sort[0] - reconstruction_loop(values, prev, next, strides, current, image_stride) + reconstruction_loop(values, prev, next, nb_strides, current, image_stride) # Reshape the values array to the shape of the padded image # and return the unpadded portion of that result From 29c84a8a7bea09dae0dc4fa419ed0bd89436e127 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 17 Aug 2012 23:02:08 -0400 Subject: [PATCH 244/648] STY: Rename returned image to distinguish input from output. --- skimage/morphology/greyreconstruct.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 8c12a9c5..c68e6e24 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -136,15 +136,13 @@ def reconstruction(image, mask, selem=None, offset=None): # Create a rank-order value array so that the Cython inner-loop # can operate on a uniform data type - # fragile: `reconstruction_loop` needs 'uint32' conversion by `rank_order` - values, value_map = rank_order(values) + rec_img, value_map = rank_order(values) current = value_sort[0] - reconstruction_loop(values, prev, next, nb_strides, current, image_stride) + reconstruction_loop(rec_img, prev, next, nb_strides, current, image_stride) - # Reshape the values array to the shape of the padded image - # and return the unpadded portion of that result - values = value_map[values[:image_stride]] - values.shape = np.array(image.shape) + 2 * padding - return values[inside_slices] + # Reshape reconstructed image to original image shape and remove padding. + rec_img = value_map[rec_img[:image_stride]] + rec_img.shape = np.array(image.shape) + 2 * padding + return rec_img[inside_slices] From 2724f50b4c23aed4446688f4107bdc55d960165e Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Sat, 18 Aug 2012 10:12:22 -0400 Subject: [PATCH 245/648] FIX: added default identity matrix to SimilarityTransform to match AffineTransform --- skimage/transform/_geometric.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 51ff9519..90a3ab57 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -298,10 +298,8 @@ class AffineTransform(ProjectiveTransform): ]) self._matrix[0:2, 2] = translation else: - # -- Default to an identity transform - self._matrix = np.asarray( - [[1, 0, 0], [0, 1, 0], [0, 0, 0]], - dtype='float64') + # Default to an identity transform + self._matrix = np.eye(3) @property def scale(self): @@ -377,6 +375,9 @@ class SimilarityTransform(ProjectiveTransform): ]) self._matrix *= scale self._matrix[0:2, 2] = translation + else: + # Default to an identity transform + self._matrix = np.eye(3) def estimate(self, src, dst): """Set the transformation matrix with the explicit parameters. From 969772c036e80930d5f36ee46e52c6e1085bc908 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 18 Aug 2012 17:59:44 -0400 Subject: [PATCH 246/648] ENH: Add reconstruction by erosion. --- skimage/morphology/greyreconstruct.py | 54 ++++++++++++++++++--------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index c68e6e24..d1434246 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -14,14 +14,12 @@ import numpy as np from skimage.filter.rank_order import rank_order -def reconstruction(image, mask, selem=None, offset=None): +def reconstruction(image, mask, selem=None, offset=None, method='dilation'): """Perform a morphological reconstruction of an image. - Reconstruction requires a "seed" image and a "mask" image. Currently, this - only implements reconstruction by dilation, such that the seed image is - dilated until it is constrained by the mask. Thus, he "seed" and "mask" - images will be the minimum and maximum possible values of the reconstructed - image, respectively. + Reconstruction requires a "seed" image and a "mask" image of equal shape. + These images set the minimum and maximum possible values of the + reconstructed image. Parameters ---------- @@ -31,6 +29,11 @@ def reconstruction(image, mask, selem=None, offset=None): The maximum allowed value at each point. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. + method : {'dilation'|'erosion'} + Perform reconstruction by dilation or erosion. In dilation (erosion), + the seed image is dilated (eroded) until limited by the mask image. + For dilation, each seed value must be less than or equal to the + corresponding mask value; for erosion, the reverse is true. Returns ------- @@ -48,7 +51,6 @@ def reconstruction(image, mask, selem=None, offset=None): [1] Vincent, L., "Morphological Grayscale Reconstruction in Image Analysis: Applications and Efficient Algorithms", IEEE Transactions on Image Processing (1993) - [2] Soille, P., "Morphological Image Analysis: Principles and Applications", Chapter 6, 2nd edition (2003), ISBN 3540429883. @@ -62,7 +64,7 @@ def reconstruction(image, mask, selem=None, offset=None): we want to extract: >>> import numpy as np - >>> from scikits.image.morphology.grey import grey_reconstruction + >>> from skimage.morphology import reconstruction >>> y, x = np.mgrid[:20:0.5, :20:0.5] >>> bumps = np.sin(x) + np.sin(y) @@ -71,7 +73,7 @@ def reconstruction(image, mask, selem=None, offset=None): >>> h = 0.3 >>> seed = bumps - h - >>> rec = grey_reconstruction(seed, bumps) + >>> rec = reconstruction(seed, bumps) The resulting reconstructed image looks exactly like the original image, but with the peaks of the bumps cut off. Subtracting this reconstructed @@ -86,7 +88,12 @@ def reconstruction(image, mask, selem=None, offset=None): """ assert tuple(image.shape) == tuple(mask.shape) - assert np.all(image <= mask) + if method == 'dilation' and np.any(image > mask): + raise ValueError("Intensity of seed image must be less than that " + "of the mask image for reconstruction by dilation.") + elif method == 'erosion' and np.any(image < mask): + raise ValueError("Intensity of seed image must be greater than that " + "of the mask image for reconstruction by erosion.") try: from ._greyreconstruct import reconstruction_loop except ImportError: @@ -112,7 +119,11 @@ def reconstruction(image, mask, selem=None, offset=None): inside_slices = [slice(p, -p) for p in padding] # Set padded region to minimum image intensity and mask along first axis so # we can interleave image and mask pixels when sorting. - values = np.ones(dims) * np.min(image) + if method == 'dilation': + pad_value = np.min(image) + elif method == 'erosion': + pad_value = np.max(image) + values = np.ones(dims) * pad_value values[[0] + inside_slices] = image values[[1] + inside_slices] = mask @@ -126,23 +137,30 @@ def reconstruction(image, mask, selem=None, offset=None): nb_strides = np.array([np.sum(value_stride * selem_offset) for selem_offset in selem_offsets], np.int32) values = values.flatten() - value_sort = np.lexsort([-values]).astype(np.int32) + index_sorted = np.argsort(-values).astype(np.int32) + if method == 'erosion': + index_sorted = index_sorted[::-1] # Make a linked list of pixels sorted by value. -1 is the list terminator. prev = -np.ones(len(values), np.int32) next = -np.ones(len(values), np.int32) - prev[value_sort[1:]] = value_sort[:-1] - next[value_sort[:-1]] = value_sort[1:] + prev[index_sorted[1:]] = index_sorted[:-1] + next[index_sorted[:-1]] = index_sorted[1:] # Create a rank-order value array so that the Cython inner-loop # can operate on a uniform data type - rec_img, value_map = rank_order(values) - current = value_sort[0] + if method == 'dilation': + value_rank, value_map = rank_order(values) + elif method == 'erosion': + value_rank, value_map = rank_order(-values) + value_map = -value_map + current = index_sorted[0] - reconstruction_loop(rec_img, prev, next, nb_strides, current, image_stride) + reconstruction_loop(value_rank, prev, next, nb_strides, current, + image_stride) # Reshape reconstructed image to original image shape and remove padding. - rec_img = value_map[rec_img[:image_stride]] + rec_img = value_map[value_rank[:image_stride]] rec_img.shape = np.array(image.shape) + 2 * padding return rec_img[inside_slices] From 7a56a7f35ee41813d42d6f1bfb3447fa0c60d569 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 18 Aug 2012 18:04:01 -0400 Subject: [PATCH 247/648] DOC: Clarify code comments and docstring --- skimage/morphology/_greyreconstruct.pyx | 61 +++++++++++++++++-------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/skimage/morphology/_greyreconstruct.pyx b/skimage/morphology/_greyreconstruct.pyx index 18197812..a59143f5 100644 --- a/skimage/morphology/_greyreconstruct.pyx +++ b/skimage/morphology/_greyreconstruct.pyx @@ -18,26 +18,47 @@ cimport cython @cython.boundscheck(False) def reconstruction_loop(np.ndarray[dtype=np.uint32_t, ndim=1, - negative_indices = False, - mode = 'c'] avalues, + negative_indices=False, mode='c'] avalues, np.ndarray[dtype=np.int32_t, ndim=1, - negative_indices = False, - mode = 'c'] aprev, + negative_indices=False, mode='c'] aprev, np.ndarray[dtype=np.int32_t, ndim=1, - negative_indices = False, - mode = 'c'] anext, + negative_indices=False, mode='c'] anext, np.ndarray[dtype=np.int32_t, ndim=1, - negative_indices = False, - mode = 'c'] astrides, + negative_indices=False, mode='c'] astrides, np.int32_t current, int image_stride): - """The inner loop for reconstruction""" + """The inner loop for reconstruction. + + This algorithm uses the rank-order of pixels. If low intensity pixels have + a low rank and high intensity pixels have a high rank, then this loop + performs reconstruction by dilation. If this ranking is reversed, the + result is reconstruction by erosion. + + For each pixel in the seed image, check its neighbors. If its neighbor's + rank is below that of the current pixel, replace the neighbor's rank with + the rank of the current pixel. This dilation is limited by the mask, i.e. + the rank at each pixel cannot exceed the mask as that pixel. + + Parameters + ---------- + avalues : array + The rank order of the flattened seed and mask images. + aprev, anext: arrays + Indices of previous and next pixels in rank sorted order. + astrides : array + Strides to neighbors of the current pixel. + current : int + Index of lowest-ranked pixel used as starting point in reconstruction + loop. + image_stride : int + Stride between seed image and mask image in `avalues`. + """ cdef: np.int32_t neighbor np.uint32_t neighbor_value np.uint32_t current_value np.uint32_t mask_value - np.int32_t link + np.int32_t current_link int i np.int32_t nprev np.int32_t nnext @@ -55,18 +76,18 @@ def reconstruction_loop(np.ndarray[dtype=np.uint32_t, ndim=1, for i in range(nstrides): neighbor = current + strides[i] neighbor_value = values[neighbor] - # Only do neighbors less than the current value + # Only propagate neighbors ranked below the current rank if neighbor_value < current_value: mask_value = values[neighbor + image_stride] - # Only do neighbors less than the mask value + # Only propagate neighbors ranked below the mask rank if neighbor_value < mask_value: - # Raise the neighbor to the mask value if - # the mask is less than current + # Raise the neighbor to the mask rank if + # the mask ranked below the current rank if mask_value < current_value: - link = neighbor + image_stride + current_link = neighbor + image_stride values[neighbor] = mask_value else: - link = current + current_link = current values[neighbor] = current_value # unlink the neighbor nprev = prev[neighbor] @@ -74,12 +95,12 @@ def reconstruction_loop(np.ndarray[dtype=np.uint32_t, ndim=1, next[nprev] = nnext if nnext != -1: prev[nnext] = nprev - # link the neighbor after the link - nnext = next[link] + # link to the neighbor after the current link + nnext = next[current_link] next[neighbor] = nnext - prev[neighbor] = link + prev[neighbor] = current_link if nnext >= 0: prev[nnext] = neighbor - next[link] = neighbor + next[current_link] = neighbor current = next[current] From ab7626da3deae657e3d03af861cfd9187fea2e20 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 18 Aug 2012 19:21:55 -0400 Subject: [PATCH 248/648] STY: Rename variables for clarity. In particular, it wasn't clear whether `image` was the seed image or the mask image rnd `values` was used to refer to both image intensity values and their rank-order. --- skimage/morphology/_greyreconstruct.pyx | 62 ++++++++++++------------- skimage/morphology/greyreconstruct.py | 61 ++++++++++++------------ 2 files changed, 62 insertions(+), 61 deletions(-) diff --git a/skimage/morphology/_greyreconstruct.pyx b/skimage/morphology/_greyreconstruct.pyx index a59143f5..dff670bb 100644 --- a/skimage/morphology/_greyreconstruct.pyx +++ b/skimage/morphology/_greyreconstruct.pyx @@ -18,14 +18,14 @@ cimport cython @cython.boundscheck(False) def reconstruction_loop(np.ndarray[dtype=np.uint32_t, ndim=1, - negative_indices=False, mode='c'] avalues, + negative_indices=False, mode='c'] rank_array, np.ndarray[dtype=np.int32_t, ndim=1, negative_indices=False, mode='c'] aprev, np.ndarray[dtype=np.int32_t, ndim=1, negative_indices=False, mode='c'] anext, np.ndarray[dtype=np.int32_t, ndim=1, negative_indices=False, mode='c'] astrides, - np.int32_t current, + np.int32_t current_idx, int image_stride): """The inner loop for reconstruction. @@ -41,66 +41,66 @@ def reconstruction_loop(np.ndarray[dtype=np.uint32_t, ndim=1, Parameters ---------- - avalues : array + rank_array : array The rank order of the flattened seed and mask images. aprev, anext: arrays Indices of previous and next pixels in rank sorted order. astrides : array Strides to neighbors of the current pixel. - current : int + current_idx : int Index of lowest-ranked pixel used as starting point in reconstruction loop. image_stride : int - Stride between seed image and mask image in `avalues`. + Stride between seed image and mask image in `rank_array`. """ cdef: - np.int32_t neighbor - np.uint32_t neighbor_value - np.uint32_t current_value - np.uint32_t mask_value + np.int32_t neighbor_idx + np.uint32_t neighbor_rank + np.uint32_t current_rank + np.uint32_t mask_rank np.int32_t current_link int i np.int32_t nprev np.int32_t nnext int nstrides = astrides.shape[0] - np.uint32_t *values = (avalues.data) + np.uint32_t *ranks = (rank_array.data) np.int32_t *prev = (aprev.data) np.int32_t *next = (anext.data) np.int32_t *strides = (astrides.data) - while current != -1: - if current < image_stride: - current_value = values[current] - if current_value == 0: + while current_idx != -1: + if current_idx < image_stride: + current_rank = ranks[current_idx] + if current_rank == 0: break for i in range(nstrides): - neighbor = current + strides[i] - neighbor_value = values[neighbor] + neighbor_idx = current_idx + strides[i] + neighbor_rank = ranks[neighbor_idx] # Only propagate neighbors ranked below the current rank - if neighbor_value < current_value: - mask_value = values[neighbor + image_stride] + if neighbor_rank < current_rank: + mask_rank = ranks[neighbor_idx + image_stride] # Only propagate neighbors ranked below the mask rank - if neighbor_value < mask_value: + if neighbor_rank < mask_rank: # Raise the neighbor to the mask rank if # the mask ranked below the current rank - if mask_value < current_value: - current_link = neighbor + image_stride - values[neighbor] = mask_value + if mask_rank < current_rank: + current_link = neighbor_idx + image_stride + ranks[neighbor_idx] = mask_rank else: - current_link = current - values[neighbor] = current_value + current_link = current_idx + ranks[neighbor_idx] = current_rank # unlink the neighbor - nprev = prev[neighbor] - nnext = next[neighbor] + nprev = prev[neighbor_idx] + nnext = next[neighbor_idx] next[nprev] = nnext if nnext != -1: prev[nnext] = nprev # link to the neighbor after the current link nnext = next[current_link] - next[neighbor] = nnext - prev[neighbor] = current_link + next[neighbor_idx] = nnext + prev[neighbor_idx] = current_link if nnext >= 0: - prev[nnext] = neighbor - next[current_link] = neighbor - current = next[current] + prev[nnext] = neighbor_idx + next[current_link] = neighbor_idx + current_idx = next[current_idx] diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index d1434246..042d86f2 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -1,6 +1,6 @@ """ -`reconstruction` originally part of CellProfiler, code licensed under both GPL -and BSD licenses. +This morphological reconstruction routine was adapted from CellProfiler, code +licensed under both GPL and BSD licenses. Website: http://www.cellprofiler.org Copyright (c) 2003-2009 Massachusetts Institute of Technology @@ -14,7 +14,7 @@ import numpy as np from skimage.filter.rank_order import rank_order -def reconstruction(image, mask, selem=None, offset=None, method='dilation'): +def reconstruction(seed, mask, selem=None, offset=None, method='dilation'): """Perform a morphological reconstruction of an image. Reconstruction requires a "seed" image and a "mask" image of equal shape. @@ -23,7 +23,7 @@ def reconstruction(image, mask, selem=None, offset=None, method='dilation'): Parameters ---------- - image : ndarray + seed : ndarray The seed image; a.k.a. marker image. mask : ndarray The maximum allowed value at each point. @@ -87,11 +87,11 @@ def reconstruction(image, mask, selem=None, offset=None, method='dilation'): transforms, but don't require a structuring element. """ - assert tuple(image.shape) == tuple(mask.shape) - if method == 'dilation' and np.any(image > mask): + assert tuple(seed.shape) == tuple(mask.shape) + if method == 'dilation' and np.any(seed > mask): raise ValueError("Intensity of seed image must be less than that " "of the mask image for reconstruction by dilation.") - elif method == 'erosion' and np.any(image < mask): + elif method == 'erosion' and np.any(seed < mask): raise ValueError("Intensity of seed image must be greater than that " "of the mask image for reconstruction by erosion.") try: @@ -100,7 +100,7 @@ def reconstruction(image, mask, selem=None, offset=None, method='dilation'): raise ImportError("_greyreconstruct extension not available.") if selem is None: - selem = np.ones([3]*image.ndim, dtype=bool) + selem = np.ones([3] * seed.ndim, dtype=bool) else: selem = selem.copy() @@ -113,54 +113,55 @@ def reconstruction(image, mask, selem=None, offset=None, method='dilation'): # Make padding for edges of reconstructed image so we can ignore boundaries padding = (np.array(selem.shape) / 2).astype(int) - dims = np.zeros(image.ndim + 1, dtype=int) - dims[1:] = np.array(image.shape) + 2 * padding + dims = np.zeros(seed.ndim + 1, dtype=int) + dims[1:] = np.array(seed.shape) + 2 * padding dims[0] = 2 inside_slices = [slice(p, -p) for p in padding] # Set padded region to minimum image intensity and mask along first axis so # we can interleave image and mask pixels when sorting. if method == 'dilation': - pad_value = np.min(image) + pad_value = np.min(seed) elif method == 'erosion': - pad_value = np.max(image) - values = np.ones(dims) * pad_value - values[[0] + inside_slices] = image - values[[1] + inside_slices] = mask + pad_value = np.max(seed) + images = np.ones(dims) * pad_value + images[[0] + inside_slices] = seed + images[[1] + inside_slices] = mask # Create a list of strides across the array to get the neighbors within # a flattened array - value_stride = np.array(values.strides[1:]) / values.dtype.itemsize - image_stride = values.strides[0] / values.dtype.itemsize + value_stride = np.array(images.strides[1:]) / images.dtype.itemsize + image_stride = images.strides[0] / images.dtype.itemsize selem_mgrid = np.mgrid[[slice(-o, d - o) for d, o in zip(selem.shape, offset)]] selem_offsets = selem_mgrid[:, selem].transpose() nb_strides = np.array([np.sum(value_stride * selem_offset) for selem_offset in selem_offsets], np.int32) - values = values.flatten() - index_sorted = np.argsort(-values).astype(np.int32) - if method == 'erosion': + + images = images.flatten() + + # Erosion goes smallest to largest; dilation goes largest to smallest. + index_sorted = np.argsort(images).astype(np.int32) + if method == 'dilation': index_sorted = index_sorted[::-1] # Make a linked list of pixels sorted by value. -1 is the list terminator. - prev = -np.ones(len(values), np.int32) - next = -np.ones(len(values), np.int32) + prev = -np.ones(len(images), np.int32) + next = -np.ones(len(images), np.int32) prev[index_sorted[1:]] = index_sorted[:-1] next[index_sorted[:-1]] = index_sorted[1:] - # Create a rank-order value array so that the Cython inner-loop - # can operate on a uniform data type + # Cython inner-loop compares the rank of pixel values. if method == 'dilation': - value_rank, value_map = rank_order(values) + value_rank, value_map = rank_order(images) elif method == 'erosion': - value_rank, value_map = rank_order(-values) + value_rank, value_map = rank_order(-images) value_map = -value_map - current = index_sorted[0] - reconstruction_loop(value_rank, prev, next, nb_strides, current, - image_stride) + start = index_sorted[0] + reconstruction_loop(value_rank, prev, next, nb_strides, start, image_stride) # Reshape reconstructed image to original image shape and remove padding. rec_img = value_map[value_rank[:image_stride]] - rec_img.shape = np.array(image.shape) + 2 * padding + rec_img.shape = np.array(seed.shape) + 2 * padding return rec_img[inside_slices] From 79fca0e20d0dc10b10f2de2116f96cd064e83f36 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 18 Aug 2012 21:54:06 -0400 Subject: [PATCH 249/648] DOC: Reorder docstring sections. --- skimage/morphology/greyreconstruct.py | 43 +++++++++++---------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 042d86f2..04ea54a3 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -40,29 +40,11 @@ def reconstruction(seed, mask, selem=None, offset=None, method='dilation'): reconstructed : ndarray The result of morphological reconstruction. - Notes - ----- - The algorithm is taken from: - Robinson, "Efficient morphological reconstruction: a downhill filter", - Pattern Recognition Letters 25 (2004) 1759-1767. - - Applications for greyscale reconstruction are discussed in: - - [1] Vincent, L., "Morphological Grayscale Reconstruction in Image Analysis: - Applications and Efficient Algorithms", IEEE Transactions on Image - Processing (1993) - [2] Soille, P., "Morphological Image Analysis: Principles and Applications", - Chapter 6, 2nd edition (2003), ISBN 3540429883. - Examples -------- - Uses for greyscale reconstruction are described in Vincent (1993). For - example, let's try to extract the features of an image by subtracting a + Here, we try to extract the bright features of an image by subtracting a background image created by reconstruction. - First, create an image where the "bumps" are the features that - we want to extract: - >>> import numpy as np >>> from skimage.morphology import reconstruction >>> y, x = np.mgrid[:20:0.5, :20:0.5] @@ -73,19 +55,30 @@ def reconstruction(seed, mask, selem=None, offset=None, method='dilation'): >>> h = 0.3 >>> seed = bumps - h - >>> rec = reconstruction(seed, bumps) + >>> background = reconstruction(seed, bumps) The resulting reconstructed image looks exactly like the original image, but with the peaks of the bumps cut off. Subtracting this reconstructed image from the original image leaves just the peaks of the bumps - >>> hdome = bumps - rec + >>> hdome = bumps - background - This operation is known as the h-dome of the image, which leaves features - of height `h` in the subtracted image. The h-dome transform, and its - inverse h-basin, are analogous to the white top-hat and black top-hat - transforms, but don't require a structuring element. + This operation is known as the h-dome of the image and leaves features + of height `h` in the subtracted image. + Notes + ----- + The algorithm is taken from: + [1] Robinson, "Efficient morphological reconstruction: a downhill filter", + Pattern Recognition Letters 25 (2004) 1759-1767. + + Applications for greyscale reconstruction are discussed in: + + [2] Vincent, L., "Morphological Grayscale Reconstruction in Image Analysis: + Applications and Efficient Algorithms", IEEE Transactions on Image + Processing (1993) + [3] Soille, P., "Morphological Image Analysis: Principles and Applications", + Chapter 6, 2nd edition (2003), ISBN 3540429883. """ assert tuple(seed.shape) == tuple(mask.shape) if method == 'dilation' and np.any(seed > mask): From 2e87dd7a3ca00f7002e86d30d6a32d6b0f18a363 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 18 Aug 2012 23:06:08 -0400 Subject: [PATCH 250/648] ENH: Add examples of morphological reconstruction. --- doc/examples/plot_fill_holes.py | 70 +++++++++++++++++++++++++++++++++ doc/examples/plot_find_spots.py | 69 ++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 doc/examples/plot_fill_holes.py create mode 100644 doc/examples/plot_find_spots.py diff --git a/doc/examples/plot_fill_holes.py b/doc/examples/plot_fill_holes.py new file mode 100644 index 00000000..7257d2bb --- /dev/null +++ b/doc/examples/plot_fill_holes.py @@ -0,0 +1,70 @@ +""" +========== +Fill holes +========== + +In this example, we fill holes (i.e. isolated, dark spots) in an image using +morphological reconstruction by erosion. Erosion expands the minimal values of +the seed image until it encounters a mask image. Thus, the seed image and mask +image represent the maximum and minimum possible values of the reconstructed +image. + +We start with an image containing both peaks and holes: +""" +import matplotlib.pyplot as plt + +from skimage import data +from skimage.exposure import rescale_intensity + +image = data.moon() +# Rescale image intensity so that we can see dim features. +image = rescale_intensity(image, in_range=(50, 200)) + +# convenience function for plotting images +def imshow(image, **kwargs): + plt.figure(figsize=(5, 4)) + plt.imshow(image, **kwargs) + plt.axis('off') + +imshow(image) +plt.title('original image') + +""" +.. image:: PLOT2RST.current_figure + +Now we need to create the seed image, where the minima represent the starting +points for erosion. To fill holes, we initialize the seed image to the maximum +value of the original image. Along the borders, however, we use the original +values of the image. These border pixels will be the starting points for the +erosion process. We then limit the erosion by setting the mask to the values +of the original image. +""" + +import numpy as np +from skimage.morphology import reconstruction + +seed = np.copy(image) +seed[1:-1, 1:-1] = image.max() +mask = image + +filled = reconstruction(seed, mask, method='erosion') + +imshow(filled, vmin=image.min(), vmax=image.max()) +plt.title('after filling holes') + +""" +.. image:: PLOT2RST.current_figure + +As shown above, eroding inward from the edges removes holes, since (by +definition) holes are surrounded by pixels of brighter value. Finally, we can +isolate the dark regions by subtracting the reconstructed image from the +original image. +""" + +imshow(image - filled) +plt.title('dark holes') +plt.show() + +""" +.. image:: PLOT2RST.current_figure +""" diff --git a/doc/examples/plot_find_spots.py b/doc/examples/plot_find_spots.py new file mode 100644 index 00000000..ff6e2196 --- /dev/null +++ b/doc/examples/plot_find_spots.py @@ -0,0 +1,69 @@ +""" +========== +Find spots +========== + +In this example, we find bright spots in an image using morphological +reconstruction by dilation. Dilation expands the maximal values of the seed +image until it encounters a mask image. Thus, the seed image and mask image +represent the minimum and maximum possible values of the reconstructed image. + +We start with an image containing both peaks and holes: +""" +import matplotlib.pyplot as plt + +from skimage import data +from skimage.exposure import rescale_intensity + +image = data.moon() +# Rescale image intensity so that we can see dim features. +image = rescale_intensity(image, in_range=(50, 200)) + +# convenience function for plotting images +def imshow(image, **kwargs): + plt.figure(figsize=(5, 4)) + plt.imshow(image, **kwargs) + plt.axis('off') + +imshow(image) +plt.title('original image') + +""" +.. image:: PLOT2RST.current_figure + +Now we need to create the seed image, where the maxima represent the starting +points for dilation. To find spots, we initialize the seed image to the minimum +value of the original image. Along the borders, however, we use the original +values of the image. These border pixels will be the starting points for the +dilation process. We then limit the dilation by setting the mask to the values +of the original image. +""" + +import numpy as np +from skimage.morphology import reconstruction + +seed = np.copy(image) +seed[1:-1, 1:-1] = image.min() +mask = image + +rec = reconstruction(seed, mask, method='dilation') + +imshow(rec, vmin=image.min(), vmax=image.max()) +plt.title('') + +""" +.. image:: PLOT2RST.current_figure + +As shown above, dilating inward from the edges removes peaks, since (by +definition) peaks are surrounded by pixels of darker value. Finally, we can +isolate the bright spots by subtracting the reconstructed image from the +original image. +""" + +imshow(image - rec) +plt.title('"holes"') +plt.show() + +""" +.. image:: PLOT2RST.current_figure +""" From 3ad1ed3a285cff9abfe29a50934ef0ffc2bf6d13 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 18 Aug 2012 23:09:47 -0400 Subject: [PATCH 251/648] DOC: Remove peak detection tutorial. The tutorial needs a lot of work and isn't a crucial part of this PR. Note: tutorial saved in a separate branch. --- .../plot_peak_detection_comparison.py | 231 ------------------ skimage/data/noisy_circles.jpg | Bin 26206 -> 0 bytes 2 files changed, 231 deletions(-) delete mode 100644 doc/examples/applications/plot_peak_detection_comparison.py delete mode 100644 skimage/data/noisy_circles.jpg diff --git a/doc/examples/applications/plot_peak_detection_comparison.py b/doc/examples/applications/plot_peak_detection_comparison.py deleted file mode 100644 index 8fe26c02..00000000 --- a/doc/examples/applications/plot_peak_detection_comparison.py +++ /dev/null @@ -1,231 +0,0 @@ -""" -============== -Peak detection -============== - -Peak detection (a.k.a. spot detection or particle detection) is a common -image analysis step. For example, it's used to detect tracer particles in a -flow for particle image velocimetry (i.e. PIV) and to identify features in -the Hough transform. - -To simplify plotting code, let's define a simple function that creates a new -figure on each call, and removes tick labels. -""" - -import matplotlib.pyplot as plt - -plt.rcParams['axes.titlesize'] = 10 -plt.rcParams['font.size'] = 10 -def imshow(image, **kwargs): - plt.figure(figsize=(2.5, 2.5)) - plt.imshow(image, **kwargs) - plt.axis('off') - -""" -To explore different peak detection techniques, we use an image of circles with -added noise: -""" - -from skimage import data -img = data.load('noisy_circles.jpg') -imshow(img) - -""" - -.. image:: PLOT2RST.current_figure - -This image is noisy and has uneven background illumination. The peaks in the -image, while readily identified by eye, can be tricky to find algorithmically. -The first thing we need to do is remove the high-frequency noise; this can -be done with a simple Gaussian filter. -""" - -import scipy.ndimage as ndimg -img_smooth = ndimg.gaussian_filter(img, 3) - -imshow(img_smooth) - -""" - -.. image:: PLOT2RST.current_figure - -Thresholding -============ - -One way to extract the background is to threshold the image. -""" - -thresh_value = 100 -background = img_smooth.copy() -background[img_smooth > thresh_value] = 0 -peaks = img_smooth - background - -""" -Here, all pixels values below the threshold value are subtracted from the -image. The resulting background image and the extracted peaks are shown below. -""" - -imshow(background, vmin=0, vmax=255) -plt.title("background image (thresholding)") - -""" -.. image:: PLOT2RST.current_figure -""" - -imshow(peaks, vmin=0, vmax=255) -plt.title("peaks (thresholding)") - -""" -.. image:: PLOT2RST.current_figure - -Because of uneven illumination, peaks on the right bleed into each other. -Increasing the threshold will fix this problem, but it will also cause some -peaks on the left to go undetected. - -Morphological reconstruction -============================ - -Morphological reconstruction uses two images: a seed image and a mask image. -Initially, all values of the reconstructed image start off pixel at the values -of the seed image. A seed pixels of high intensity spread outwards until it -hits the mask image (i.e. the mask value for a pixel is lower than the -high-intensity value). Note that the mask is a gray-scale image that limits the -maximum intensity at a pixel. This algorithm is clearer with pictures, which -we generate below. - -One common case uses mask and seed images derived from the same image but -shifted in intensity. Note: be careful when shifting images integer values, -since this can lead to under/overflow of values. To prevent the uint8 image we -started with from underflowing during subtraction, we first convert to float: -""" - -from skimage import img_as_float -img_r = img_as_float(img_smooth) - -import skimage.morphology as morph -h = 0.1 -rec = morph.reconstruction(img_r-h, img_r) - -imshow(img_r, vmin=0, vmax=1) -plt.title("original (smoothed) image") - -""" -.. image:: PLOT2RST.current_figure -""" - -imshow(rec, vmin=0, vmax=1) -plt.title("background image (reconstruction)") - -""" -.. image:: PLOT2RST.current_figure - -This reconstructed image looks pretty much like the original, except that the -peaks in the image are truncated. The reconstructed image can then be -subtracted from the original image to reveal the peaks of the image. -""" - -imshow(img_r-rec) -plt.title("h-dome of image") - -""" -.. image:: PLOT2RST.current_figure - -The result is known as the h-dome transformation [2]_, which extracts peaks of -height `h` from the original image. To better understand what's going on, -let's take a 1D slice along the middle of the image (cutting through peaks in -the image). -""" - -img_slice = img_r[99:100, :] -rec_slice = morph.reconstruction(img_slice-h, img_slice) - -""" -Plotting the reconstructed image (slice) next to the original image and the -seed image shed light on the reconstruction process -""" -plt.figure(figsize=(4, 3)) -plt.plot(img_slice[0], 'k', label='original image') -plt.plot(img_slice[0]-h, '0.5', label='seed image') -plt.plot(rec_slice[0], 'r', label='reconstructed') -plt.title("image slice") -plt.xlabel('x') -plt.ylabel('intensity') -plt.legend() - -""" -.. image:: PLOT2RST.current_figure - -Here, you see that morphological reconstruction dilates the seed image (i.e. -the `h`-shifted image) until it intersects the mask (original image). Note that -the peaks in the original image have very different intensity values (e.g. the -peak at x=200 and x=100 differ by about 80). Subtracting the reconstructed -image from the original image gives peaks of roughly equal intensity. Thus, the -h-dome transformation is quiet effective at removing uneven, dark backgrounds -from bright features. The inverse operation---the h-basin -transformation---should be used when removing bright backgrounds from dark -features. - - -White tophat -============ -""" - -selem = morph.disk(10) -opening = morph.opening(img_smooth, selem) -top_hat = img_smooth - opening - -imshow(opening, vmin=0, vmax=255) -plt.title("Greyscale opening of image") - -""" -.. image:: PLOT2RST.current_figure -""" - - -imshow(top_hat) -plt.title("Tophat with disk of r = 10") - -""" -.. image:: PLOT2RST.current_figure -""" - -selem = morph.disk(5) -top_hat = morph.white_tophat(img_smooth, selem) - -imshow(top_hat) -plt.title("Tophat with disk of r = 5") - -""" -.. image:: PLOT2RST.current_figure -""" - -selem = morph.square(20) -opening = morph.opening(img_smooth, selem) -# scikit's top hat filter uses uint8 and doesn't check for over(under)flow. -mask = opening > img_smooth -opening[mask] = img_smooth[mask] -top_hat = img_smooth - opening - -imshow(opening, vmin=0, vmax=255) -plt.title("Greyscale opening of image") - -""" -.. image:: PLOT2RST.current_figure -""" - -imshow(top_hat) -plt.title("Tophat with square of w = 10") - -plt.show() - -""" -.. image:: PLOT2RST.current_figure - - -References -========== - -.. [1] Crocker and Grier, Journal of Colloid and Interface Science (1996) -.. [2] Vincent, L., IEEE Transactions on Image Processing (1993) - -""" diff --git a/skimage/data/noisy_circles.jpg b/skimage/data/noisy_circles.jpg deleted file mode 100644 index a0f49d6767ca4ca3cfbef6c54c36039223d243ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26206 zcmeIa2UJr_*FSthBIOF9T@^x$B*3+R^b$&lAP{LPC@7*-A#_M+B81{axJp+6m7*X; znhHo$1BwDFy(!WJq?&+$L|XDc!E*2OywAVA-}=`2*7`hI=bR~f_UxJ2)ABpBnQxgN zgclA45zYaCsVT4*002&a8v+1O&;X?uv z;rsE%Ie9xcBagUxle~$p-abe@3l*fPy(7VkP7TRy-koJGfc4_pMeIXOAGIJvpF zxcRsLww-?mFE=;u4k1Co9fE>F{M=jD_t!Vizds>7+qd(;dHCRPzTe?+`0uO-{P%B3 z`2SBBFzW$+P9Ph|hC(C(Hhu_{AHp003;7$f5rPG!+y>dYK!I5o6o9b7wy|??a{aa) z^6!xlfDQU1k`I8uAW$|K+cpk%&TUX`B`}g73fnE9unl+AUQp6ELXrLVq}vY;qR=~z zH8?0K`$cx@aY!XwEPVLvNL1N*=V6qzeu|~jp=UWH)m>f3$D9|-FFx<~A73(%xtjZD z#kIzsiRHVMFM2{bwfffD#i3T{)3Ncx>6LBp}m1tqk@2fxT?{E|q*b1EfkNc$ayoQe zmE=73Q-uGy1Tue%z#IU$p<8+K16W`=Js*+RttVo8T&1M+5zto|xANeMOQ++hzIE$| z57%;T=9uu`R%*{$y=QBVa%$+AtE#_{s+5{`dST=uAMC=Hjh?F`<)t=?{Ta3mQ+m`u z@7X(!{+B&2MCILttpqx^Vy3E7#acf{4T>)okJ}Vvp!J)3rcvwoJyeDA!4EVfeeqN2 zl@ev-%Rha;`13tOCZ6LJP53f+EyKm#s7pj
  • F%n8k9it%ULC>PP!Y|GKDK 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 Q%i7-Ox?=fb6z04C0pd5rzW@LL From b614b34eab22d8d06e87c34426229ba1fb352324 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 18 Aug 2012 23:20:49 -0400 Subject: [PATCH 252/648] ENH: Add test of reconstruction by erosion. --- skimage/morphology/tests/test_reconstruction.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/skimage/morphology/tests/test_reconstruction.py b/skimage/morphology/tests/test_reconstruction.py index a3461f31..74f61b9f 100644 --- a/skimage/morphology/tests/test_reconstruction.py +++ b/skimage/morphology/tests/test_reconstruction.py @@ -8,6 +8,7 @@ All rights reserved. Original author: Lee Kamentsky """ import numpy as np +from numpy.testing import assert_array_almost_equal as assert_close from skimage.morphology.greyreconstruct import reconstruction @@ -68,6 +69,14 @@ def test_zero_image_one_mask(): assert np.all(result == 0) +def test_fill_hole(): + """Test reconstruction by erosion, which should fill holes in mask.""" + seed = np.array([0, 8, 8, 8, 8, 8, 8, 8, 8, 0]) + mask = np.array([0, 3, 6, 2, 1, 1, 1, 4, 2, 0]) + result = reconstruction(seed, mask, method='erosion') + assert_close(result, np.array([0, 3, 6, 4, 4, 4, 4, 4, 2, 0])) + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() From 195bdc8c00fc66d72ed841a3cf12caa904882584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 19 Aug 2012 13:02:03 +0200 Subject: [PATCH 253/648] complete default matrix initialization of geometric transforms --- skimage/transform/_geometric.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 90a3ab57..c055247e 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -105,7 +105,10 @@ class ProjectiveTransform(GeometricTransform): coeffs = range(8) def __init__(self, matrix=None): - if matrix is not None and matrix.shape != (3, 3): + if matrix is None: + # default to an identity transform + matrix = np.eye(3) + if matrix.shape != (3, 3): raise ValueError("invalid shape of transformation matrix") self._matrix = matrix @@ -271,15 +274,16 @@ class AffineTransform(ProjectiveTransform): def __init__(self, matrix=None, scale=None, rotation=None, shear=None, translation=None): - self._matrix = matrix params = any(param is not None for param in (scale, rotation, shear, translation)) if params and matrix is not None: raise ValueError("You cannot specify the transformation matrix and " "the implicit parameters at the same time.") - elif matrix is not None and matrix.shape != (3, 3): - raise ValueError("Invalid shape of transformation matrix.") + elif matrix is not None: + if matrix.shape != (3, 3): + raise ValueError("Invalid shape of transformation matrix.") + self._matrix = matrix elif params: if scale is None: scale = (1, 1) @@ -298,7 +302,7 @@ class AffineTransform(ProjectiveTransform): ]) self._matrix[0:2, 2] = translation else: - # Default to an identity transform + # default to an identity transform self._matrix = np.eye(3) @property @@ -351,15 +355,16 @@ class SimilarityTransform(ProjectiveTransform): def __init__(self, matrix=None, scale=None, rotation=None, translation=None): - self._matrix = matrix params = any(param is not None for param in (scale, rotation, translation)) if params and matrix is not None: raise ValueError("You cannot specify the transformation matrix and " "the implicit parameters at the same time.") - elif matrix is not None and matrix.shape != (3, 3): - raise ValueError("Invalid shape of transformation matrix.") + elif matrix is not None: + if matrix.shape != (3, 3): + raise ValueError("Invalid shape of transformation matrix.") + self._matrix = matrix elif params: if scale is None: scale = 1 @@ -376,7 +381,7 @@ class SimilarityTransform(ProjectiveTransform): self._matrix *= scale self._matrix[0:2, 2] = translation else: - # Default to an identity transform + # default to an identity transform self._matrix = np.eye(3) def estimate(self, src, dst): @@ -480,7 +485,10 @@ class PolynomialTransform(GeometricTransform): """ def __init__(self, params=None): - if params is not None and params.shape[0] != 2: + if params is None: + # default to transformation which preserves original coordinates + params = np.array([[0, 1, 0], [0, 0, 1]]) + if params.shape[0] != 2: raise ValueError("invalid shape of transformation parameters") self._params = params From 6ea4827f9dd64c04b4d1da3c6f6ea85123abe481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 19 Aug 2012 13:04:58 +0200 Subject: [PATCH 254/648] add test case for polynomial transform initialization --- skimage/transform/tests/test_geometric.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index d3e7f20c..b6db1dec 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -142,6 +142,13 @@ def test_polynomial_estimation(): tform2 = PolynomialTransform() tform2.estimate(SRC, DST, order=10) assert_array_almost_equal(tform2._params, tform._params) + + +def test_polynomial_init(): + tform = estimate_transform('polynomial', SRC, DST, order=10) + # init with transformation matrix + tform2 = PolynomialTransform(tform._params) + assert_array_almost_equal(tform2._params, tform._params) def test_union(): From d511bd84295a8d07f886ca8482cfea25dfca0519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 19 Aug 2012 13:06:27 +0200 Subject: [PATCH 255/648] fix comment --- skimage/transform/tests/test_geometric.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index b6db1dec..abe8b2e7 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -142,11 +142,11 @@ def test_polynomial_estimation(): tform2 = PolynomialTransform() tform2.estimate(SRC, DST, order=10) assert_array_almost_equal(tform2._params, tform._params) - - + + def test_polynomial_init(): tform = estimate_transform('polynomial', SRC, DST, order=10) - # init with transformation matrix + # init with transformation parameters tform2 = PolynomialTransform(tform._params) assert_array_almost_equal(tform2._params, tform._params) From b5d91069664b4f323df9b2a201966a4e8ac8160d Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 19 Aug 2012 11:56:16 -0400 Subject: [PATCH 256/648] ENH: Use Cython data types instead of Numpy dtypes. Conversion to Memoryviews didn't improve performance, unfortunately. Minor slow-down of 5--10%. --- skimage/morphology/_greyreconstruct.pyx | 39 ++++++------------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/skimage/morphology/_greyreconstruct.pyx b/skimage/morphology/_greyreconstruct.pyx index dff670bb..c7dcab38 100644 --- a/skimage/morphology/_greyreconstruct.pyx +++ b/skimage/morphology/_greyreconstruct.pyx @@ -10,23 +10,13 @@ Original author: Lee Kamentsky """ from __future__ import division -import numpy as np -cimport numpy as np cimport cython @cython.boundscheck(False) -def reconstruction_loop(np.ndarray[dtype=np.uint32_t, ndim=1, - negative_indices=False, mode='c'] rank_array, - np.ndarray[dtype=np.int32_t, ndim=1, - negative_indices=False, mode='c'] aprev, - np.ndarray[dtype=np.int32_t, ndim=1, - negative_indices=False, mode='c'] anext, - np.ndarray[dtype=np.int32_t, ndim=1, - negative_indices=False, mode='c'] astrides, - np.int32_t current_idx, - int image_stride): +def reconstruction_loop(unsigned int[:] ranks, int[:] prev, int[:] next, + int[:] strides, int current_idx, int image_stride): """The inner loop for reconstruction. This algorithm uses the rank-order of pixels. If low intensity pixels have @@ -41,32 +31,21 @@ def reconstruction_loop(np.ndarray[dtype=np.uint32_t, ndim=1, Parameters ---------- - rank_array : array + ranks : array The rank order of the flattened seed and mask images. - aprev, anext: arrays + prev, next: arrays Indices of previous and next pixels in rank sorted order. - astrides : array + strides : array Strides to neighbors of the current pixel. current_idx : int Index of lowest-ranked pixel used as starting point in reconstruction loop. image_stride : int - Stride between seed image and mask image in `rank_array`. + Stride between seed image and mask image in `ranks`. """ - cdef: - np.int32_t neighbor_idx - np.uint32_t neighbor_rank - np.uint32_t current_rank - np.uint32_t mask_rank - np.int32_t current_link - int i - np.int32_t nprev - np.int32_t nnext - int nstrides = astrides.shape[0] - np.uint32_t *ranks = (rank_array.data) - np.int32_t *prev = (aprev.data) - np.int32_t *next = (anext.data) - np.int32_t *strides = (astrides.data) + cdef unsigned int neighbor_rank, current_rank, mask_rank + cdef int i, current_link, neighbor_idx, nprev, nnext + cdef int nstrides = strides.shape[0] while current_idx != -1: if current_idx < image_stride: From 682f064f86b9e3052fe48dc6bdd81f5479dc21c1 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 19 Aug 2012 16:23:14 -0400 Subject: [PATCH 257/648] DOC: Fix docstring to match rank order. --- skimage/morphology/_greyreconstruct.pyx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/skimage/morphology/_greyreconstruct.pyx b/skimage/morphology/_greyreconstruct.pyx index c7dcab38..5d2cb0c5 100644 --- a/skimage/morphology/_greyreconstruct.pyx +++ b/skimage/morphology/_greyreconstruct.pyx @@ -8,9 +8,6 @@ All rights reserved. Original author: Lee Kamentsky """ - -from __future__ import division - cimport cython @@ -38,8 +35,7 @@ def reconstruction_loop(unsigned int[:] ranks, int[:] prev, int[:] next, strides : array Strides to neighbors of the current pixel. current_idx : int - Index of lowest-ranked pixel used as starting point in reconstruction - loop. + Index of highest-ranked pixel used as starting point in loop. image_stride : int Stride between seed image and mask image in `ranks`. """ From 3fe03259d09ee8bcf4ffd31de066bde285017df1 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 19 Aug 2012 16:24:12 -0400 Subject: [PATCH 258/648] DOC: Combine examples for finding spots and filling holes. --- doc/examples/plot_find_spots.py | 69 ------------------- ..._fill_holes.py => plot_holes_and_peaks.py} | 24 +++++-- 2 files changed, 20 insertions(+), 73 deletions(-) delete mode 100644 doc/examples/plot_find_spots.py rename doc/examples/{plot_fill_holes.py => plot_holes_and_peaks.py} (73%) diff --git a/doc/examples/plot_find_spots.py b/doc/examples/plot_find_spots.py deleted file mode 100644 index ff6e2196..00000000 --- a/doc/examples/plot_find_spots.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -========== -Find spots -========== - -In this example, we find bright spots in an image using morphological -reconstruction by dilation. Dilation expands the maximal values of the seed -image until it encounters a mask image. Thus, the seed image and mask image -represent the minimum and maximum possible values of the reconstructed image. - -We start with an image containing both peaks and holes: -""" -import matplotlib.pyplot as plt - -from skimage import data -from skimage.exposure import rescale_intensity - -image = data.moon() -# Rescale image intensity so that we can see dim features. -image = rescale_intensity(image, in_range=(50, 200)) - -# convenience function for plotting images -def imshow(image, **kwargs): - plt.figure(figsize=(5, 4)) - plt.imshow(image, **kwargs) - plt.axis('off') - -imshow(image) -plt.title('original image') - -""" -.. image:: PLOT2RST.current_figure - -Now we need to create the seed image, where the maxima represent the starting -points for dilation. To find spots, we initialize the seed image to the minimum -value of the original image. Along the borders, however, we use the original -values of the image. These border pixels will be the starting points for the -dilation process. We then limit the dilation by setting the mask to the values -of the original image. -""" - -import numpy as np -from skimage.morphology import reconstruction - -seed = np.copy(image) -seed[1:-1, 1:-1] = image.min() -mask = image - -rec = reconstruction(seed, mask, method='dilation') - -imshow(rec, vmin=image.min(), vmax=image.max()) -plt.title('') - -""" -.. image:: PLOT2RST.current_figure - -As shown above, dilating inward from the edges removes peaks, since (by -definition) peaks are surrounded by pixels of darker value. Finally, we can -isolate the bright spots by subtracting the reconstructed image from the -original image. -""" - -imshow(image - rec) -plt.title('"holes"') -plt.show() - -""" -.. image:: PLOT2RST.current_figure -""" diff --git a/doc/examples/plot_fill_holes.py b/doc/examples/plot_holes_and_peaks.py similarity index 73% rename from doc/examples/plot_fill_holes.py rename to doc/examples/plot_holes_and_peaks.py index 7257d2bb..a9c3a7fe 100644 --- a/doc/examples/plot_fill_holes.py +++ b/doc/examples/plot_holes_and_peaks.py @@ -1,7 +1,7 @@ """ -========== -Fill holes -========== +=============================== +Filling holes and finding peaks +=============================== In this example, we fill holes (i.e. isolated, dark spots) in an image using morphological reconstruction by erosion. Erosion expands the minimal values of @@ -62,7 +62,23 @@ original image. """ imshow(image - filled) -plt.title('dark holes') +plt.title('holes') + +""" +.. image:: PLOT2RST.current_figure + +Alternatively, we can find bright spots in an image using morphological +reconstruction by dilation. Dilation is the inverse of erosion and expands the +*maximal* values of the seed image until it encounters a mask image. Since this +is an inverse operation, we initialize the seed image to the minimum image +intensity instead of the maximum. The remainder of the process is the same. +""" + +seed = np.copy(image) +seed[1:-1, 1:-1] = image.min() +rec = reconstruction(seed, mask, method='dilation') +imshow(image - rec) +plt.title('peaks') plt.show() """ From b1007f019675621a372066433b2a374e62c3b1b1 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 19 Aug 2012 17:46:22 -0400 Subject: [PATCH 259/648] ENH: Add regional maxima example --- doc/examples/plot_regional_maxima.py | 113 +++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 doc/examples/plot_regional_maxima.py diff --git a/doc/examples/plot_regional_maxima.py b/doc/examples/plot_regional_maxima.py new file mode 100644 index 00000000..9d4de9b1 --- /dev/null +++ b/doc/examples/plot_regional_maxima.py @@ -0,0 +1,113 @@ +""" +========================= +Filtering regional maxima +========================= + +Here, we use morphological reconstruction to create a background image, which +we can subtract from the original image to isolate bright features (regional +maxima). + +First we try reconstruction by dilation starting at the edges of the image. We +initialize a seed image to the minimum intensity of the image, and set its +border to be the pixel values in the original image. These maximal pixels will +get dilated in order to reconstruct the background image. +""" +import numpy as np + +from skimage import data +from skimage import img_as_float +from skimage.morphology import reconstruction +from scipy.ndimage import gaussian_filter +import matplotlib.pyplot as plt + +# Convert to float: Important for subtraction later which won't work with uint8 +image = img_as_float(data.coins()) +image = gaussian_filter(image, 1) + +seed = np.copy(image) +seed[1:-1, 1:-1] = image.min() +mask = image + +dilated = reconstruction(seed, mask, method='dilation') + +""" +Subtracting the dilated image leaves an image with just the coins and a flat, +black background, as shown below. +""" + +fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, figsize=(8, 2.5)) + +ax1.imshow(image) +ax1.set_title('original image') +ax1.axis('off') + +ax2.imshow(dilated, vmin=image.min(), vmax=image.max()) +ax2.set_title('dilated') +ax2.axis('off') + +ax3.imshow(image - dilated) +ax3.set_title('image - dilated') +ax3.axis('off') + +plt.tight_layout() + +""" + +.. image:: PLOT2RST.current_figure + +Although the features (i.e. the coins) are clearly isolated, the coins +surrounded by a bright background in the original image are dimmer in the +subtracted image. We can attempt to correct this using a different seed image. + +Instead of creating a seed image with maxima along the image border, we can use +the features of the image itself to seed the reconstruction process. Here, the +seed image is the original image minus a fixed value, ``h``. +""" + +h = 0.4 +seed = image - h +dilated = reconstruction(seed, mask, method='dilation') +hdome = image - dilated + +""" +To get a feel for the reconstruction process, we plot the intensity of the +mask, seed, and dilated images along a slice of the image (indicated by red +line). +""" + +fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, figsize=(8, 2.5)) + +yslice = 197 + +ax1.plot(mask[yslice], '0.5', label='mask') +ax1.plot(seed[yslice], 'k', label='seed') +ax1.plot(dilated[yslice], 'r', label='dilated') +ax1.set_ylim(-0.2, 2) +ax1.set_title('image slice') +ax1.set_xticks([]) +ax1.legend() + +ax2.imshow(dilated, vmin=image.min(), vmax=image.max()) +ax2.axhline(yslice, color='r', alpha=0.4) +ax2.set_title('dilated') +ax2.axis('off') + +ax3.imshow(hdome) +ax3.axhline(yslice, color='r', alpha=0.4) +ax3.set_title('image - dilated') +ax3.axis('off') + +plt.tight_layout() +plt.show() + +""" +.. image:: PLOT2RST.current_figure + +As you can see in the image slice, each coin is given a different baseline +intensity in the reconstructed image; this is because we used the local +intensity (shifted by ``h``) as a seed value. As a result, the coins in the +subtracted image have similar pixel intensities. The final result is known as +the h-dome of an image since this tends to isolate regional maxima of height +``h``. This operation is particularly useful when your images are unevenly +illuminated. +""" From 6410905b3b3c0a418c15e80fff6d2ce149f0c5d6 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 20 Aug 2012 07:55:17 -0700 Subject: [PATCH 260/648] BUG: Fix broken import in ctmf. --- skimage/filter/ctmf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/ctmf.py b/skimage/filter/ctmf.py index 286932be..94f273e1 100644 --- a/skimage/filter/ctmf.py +++ b/skimage/filter/ctmf.py @@ -13,7 +13,7 @@ Original author: Lee Kamentsky import numpy as np from . import _ctmf -from .rank_order import rank_order +from ._rank_order import rank_order def median_filter(image, radius=2, mask=None, percent=50): From f4263556c4e855b0b2b0dab24a0557c550682108 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 20 Aug 2012 10:43:12 -0700 Subject: [PATCH 261/648] DOC: Update development guidelines. --- DEVELOPMENT.txt | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/DEVELOPMENT.txt b/DEVELOPMENT.txt index 4b42cf37..3f10276d 100644 --- a/DEVELOPMENT.txt +++ b/DEVELOPMENT.txt @@ -9,32 +9,36 @@ Development process instructions on making your own fork. * Create a new branch for the feature you want to work on. Since the branch name will appear in the merge message, use a sensible name - such as 'your_name-transform-speedups'. + such as 'transform-speedups'. * Commit locally as you progress. * Push your changes back to github and create a pull request by clicking "request pull" in GitHub. * Optionally, mail the mailing list, explaining your changes. +.. note:: + + Do *not* merge the main branch into yours. If GitHub indicates that the + Pull Request can no longer be merged automatically, rebase onto master. + + (If you are curious, here's a further discussion on + the `dangers of rebasing `__. Also + see this `LWN article `__.) + +* To reviewers: add a short explanation of what a branch did to the merge + message or, if closing a bug, add "Closes gh-XXXX". + You may also read this summary by Fernando Perez of the IPython project on how they manage to keep review overhead to a minimum: http://mail.scipy.org/pipermail/ipython-dev/2010-October/006746.html -.. note:: - - Do *not* merge the main branch into yours. You may rebase, - as long as you are `aware of its dangers `_ - (also see `LWN article `_). - -* To reviewers: add a short explanation of what a branch did to the merge - message or, if closing a bug, add "Closes gh-XXXX". - Guidelines `````````` * All code should have tests (see "Test coverage" below for more details). * All code should be documented, to the same `standard `_ - as NumPy and SciPy. If possible, also add a section to the user guide. + as NumPy and SciPy. For new functionality, always add an example to the + gallery. * Follow the `Python PEPs `_ where possible. * No major changes should be committed without review. Ask on the @@ -42,6 +46,20 @@ Guidelines you get no response to your pull request. * Examples in the gallery should have a maximum figure width of 8 inches. +Stylistic Guidelines +```````````````````` + * Use numpy data types instead of strings (``np.uint8`` instead of + ``"uint8"``). + + * Use the following import conventions:: + + import numpy as np + import matplotlib.pyplot as plt + + cimport numpy as cnp # in Cython code + + * Set up your editor to remove trailing whitespace. + Test coverage ````````````` Tests for a module should ideally cover all code in that module, From e5bbceac1f88bbbf83a24d64b5646ecca4fb575f Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 20 Aug 2012 11:19:58 -0700 Subject: [PATCH 262/648] DOC: More coding conventions. --- DEVELOPMENT.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/DEVELOPMENT.txt b/DEVELOPMENT.txt index 3f10276d..e205af85 100644 --- a/DEVELOPMENT.txt +++ b/DEVELOPMENT.txt @@ -58,7 +58,19 @@ Stylistic Guidelines cimport numpy as cnp # in Cython code + * When documenting array parameters, use ``image : (M, N) ndarray``, + ``image : (M, N, 3) ndarray`` and then refer to ``M`` and ``N`` in the + docstring. * Set up your editor to remove trailing whitespace. + * If a function name, say ``segment(...)``, has the same name as the file in + which it is implemented, name that file ``_segment.py`` so that it can still + be imported. + * Functions should support all input image dtypes. Use utility functions such + as ``img_as_float`` to help convert to an appropriate type. The output + format can be whatever is most efficient. This allows us to string together + several functions into a pipeline, e.g.:: + + hough(canny(my_image)) Test coverage ````````````` From 37c0ffe0724531e6ab27b28038a6e7ba2030f352 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Mon, 20 Aug 2012 20:01:41 +0100 Subject: [PATCH 263/648] cosmit: changed the names of x and y to r and c. that was no fun, i tell you. --- skimage/segmentation/_quickshift.pyx | 55 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/skimage/segmentation/_quickshift.pyx b/skimage/segmentation/_quickshift.pyx index 4267c17b..a924d72f 100644 --- a/skimage/segmentation/_quickshift.pyx +++ b/skimage/segmentation/_quickshift.pyx @@ -18,7 +18,7 @@ cdef extern from "math.h": @cython.wraparound(False) @cython.cdivision(True) def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=False, - sigma=0, convert2lab=True, random_seed=None): + sigma=0, convert2lab=True, random_seed=None): """Segments image using quickshift clustering in Color-(x,y) space. Produces an oversegmentation of the image using the quickshift mode-seeking @@ -78,9 +78,6 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=Fa random_state = np.random.RandomState(random_seed) - # We compute the distances twice since otherwise - # we get crazy memory overhead (width * height * windowsize**2) - # TODO join orphaned roots? # Some nodes might not have a point of higher density within the # search window. We could do a global search over these in the end. @@ -97,7 +94,7 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=Fa cdef int channels = image_c.shape[2] cdef double current_density, closest, dist - cdef int x, y, x_, y_ + cdef int r, c, r_, c_, channel cdef np.float_t* image_p = image_c.data cdef np.float_t* current_pixel_p = image_p @@ -105,17 +102,17 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=Fa cdef np.ndarray[dtype=np.float_t, ndim=2] densities \ = np.zeros((height, width)) # compute densities - for x in range(height): - for y in range(width): - x_min, x_max = max(x - w, 0), min(x + w + 1, height) - y_min, y_max = max(y - w, 0), min(y + w + 1, width) - for x_ in range(x_min, x_max): - for y_ in range(y_min, y_max): + for r in range(height): + for c in range(width): + r_min, r_max = max(r - w, 0), min(r + w + 1, height) + c_min, c_max = max(c - w, 0), min(c + w + 1, width) + for r_ in range(r_min, r_max): + for c_ in range(c_min, c_max): dist = 0 - for c in range(channels): - dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 - dist += (x - x_)**2 + (y - y_)**2 - densities[x, y] += exp(-dist / (2 * kernel_size**2)) + for channel in range(channels): + dist += (current_pixel_p[channel] - image_c[r_, c_, channel])**2 + dist += (r - r_)**2 + (c - c_)**2 + densities[r, c] += exp(-dist / (2 * kernel_size**2)) current_pixel_p += channels # this will break ties that otherwise would give us headache @@ -128,23 +125,25 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=Fa = np.zeros((height, width)) # find nearest node with higher density current_pixel_p = image_p - for x in range(height): - for y in range(width): - current_density = densities[x, y] + for r in range(height): + for c in range(width): + current_density = densities[r, c] closest = np.inf - x_min, x_max = max(x - w, 0), min(x + w + 1, height) - y_min, y_max = max(y - w, 0), min(y + w + 1, width) - for x_ in range(x_min, x_max): - for y_ in range(y_min, y_max): - if densities[x_, y_] > current_density: + r_min, r_max = max(r - w, 0), min(r + w + 1, height) + c_min, c_max = max(c - w, 0), min(c + w + 1, width) + for r_ in range(r_min, r_max): + for c_ in range(c_min, c_max): + if densities[r_, c_] > current_density: dist = 0 - for c in range(channels): - dist += (current_pixel_p[c] - image_c[x_, y_, c])**2 - dist += (x - x_)**2 + (y - y_)**2 + # We compute the distances twice since otherwise + # we get crazy memory overhead (width * height * windowsize**2) + for channel in range(channels): + dist += (current_pixel_p[channel] - image_c[r_, c_, channel])**2 + dist += (r - r_)**2 + (c - c_)**2 if dist < closest: closest = dist - parent[x, y] = x_ * width + y_ - dist_parent[x, y] = sqrt(closest) + parent[r, c] = r_ * width + c_ + dist_parent[r, c] = sqrt(closest) current_pixel_p += channels dist_parent_flat = dist_parent.ravel() From fe2a4334faa0383fde6c29c791508151d632f704 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Mon, 20 Aug 2012 20:22:06 +0100 Subject: [PATCH 264/648] ENH addressed (hopefully all) of Tony's and Stefan's comments. --- doc/examples/plot_segmentations.py | 9 +++--- skimage/segmentation/_felzenszwalb.py | 29 ++++++++++--------- skimage/segmentation/_quickshift.pyx | 28 +++++++++--------- skimage/segmentation/_slic.pyx | 16 +++++----- skimage/segmentation/felzenszwalb_cy.pyx | 7 +++-- .../segmentation/tests/test_felzenszwalb.py | 6 ++-- 6 files changed, 48 insertions(+), 47 deletions(-) diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py index 8830cebb..09012bbd 100644 --- a/doc/examples/plot_segmentations.py +++ b/doc/examples/plot_segmentations.py @@ -28,13 +28,13 @@ Quickshift image segmentation Quickshift is a relatively recent 2d image segmentation algorithm, based on an approximation of kernelized mean-shift. Therefore it belongs to the family of local mode-seeking algorithms and is applied to the 5d space consisting of -color information and image location. see [2]_. +color information and image location [2]_. One of the benefits of quickshift is that it actually computes a hierarchical segmentation on multiple scales simultaneously. -Quickshift has three parameters: ``sigma`` controls the scale of the local -density approximation, ``max_dist`` other selecting a level in the hierarchical +Quickshift has two main parameters: ``sigma`` controls the scale of the local +density approximation, ``max_dist`` selects a level in the hierarchical segmentation that is produced. There is also a trade-off between distance in color-space and distance in image-space, given by ``ratio``. @@ -45,7 +45,7 @@ color-space and distance in image-space, given by ``ratio``. SLIC - K-Means based image segmentation --------------------------------------- -This algorithm simply performs K-kmeans in the 5d space of color information +This algorithm simply performs K-means in the 5d space of color information and image location and is therefore closely related to quickshift. As the clustering method is simpler, it is very efficient. It is essential for this algorithm to work in Lab color space to obtain good results. The algorithm @@ -57,7 +57,6 @@ of Quickshift, while ``n_segments`` chooses the number of centers for kmeans. Pascal Fua, and Sabine Suesstrunk, SLIC Superpixels Compared to State-of-the-art Superpixel Methods, TPAMI, May 2012. """ -print __doc__ import matplotlib.pyplot as plt import numpy as np diff --git a/skimage/segmentation/_felzenszwalb.py b/skimage/segmentation/_felzenszwalb.py index f84f3e56..5729bd95 100644 --- a/skimage/segmentation/_felzenszwalb.py +++ b/skimage/segmentation/_felzenszwalb.py @@ -17,24 +17,24 @@ def felzenszwalb(image, scale=1, sigma=0.8, min_size=20): controlled indirectly through ``scale``. Segment size within an image can vary greatly depending on local contrast. - Calls the algorithm on each channel separately, then combines - using "and", i.e. two pixels are in the same segment if they are - in the same segment for each channel. + For RGB images, the algorithm computes a separate segmentation for each + channel and then combines these. The combined segmentation is the + intersection of the separate segmentations on the color channels. Parameters ---------- - image: (width, height) ndarray - Input image - scale: float + image : (width, height, 3) or (width, height) ndarray + Input image. + scale : float Free parameter. Higher means larger clusters. - sigma: float + sigma : float Width of Gaussian kernel used in preprocessing. - min_size: int + min_size : int Minimum component size. Enforced using postprocessing. Returns ------- - segment_mask: ndarray, [width, height] + segment_mask : (width, height) ndarray Integer mask indicating segment labels. References @@ -49,20 +49,21 @@ def felzenszwalb(image, scale=1, sigma=0.8, min_size=20): return _felzenszwalb_grey(image, scale=scale, sigma=sigma) elif image.ndim != 3: - raise ValueError("Got image with ndim=%d, don't know" - " what to do." % image.ndim) + raise ValueError("Felzenswalb segmentation can only operate on RGB and" + " grey images, but input array of ndim %d given." + % image.ndim) # assume we got 2d image with multiple channels n_channels = image.shape[2] if n_channels != 3: warnings.warn("Got image with %d channels. Is that really what you" - " wanted?" % image.shape[2]) + " wanted?" % image.shape[2]) segmentations = [] # compute quickshift for each channel for c in xrange(n_channels): channel = np.ascontiguousarray(image[:, :, c]) s = _felzenszwalb_grey(channel, scale=scale, sigma=sigma, - min_size=min_size) + min_size=min_size) segmentations.append(s) # put pixels in same segment only if in the same segment in all images @@ -70,7 +71,7 @@ def felzenszwalb(image, scale=1, sigma=0.8, min_size=20): n0 = segmentations[0].max() + 1 n1 = segmentations[1].max() + 1 segmentation = (segmentations[0] + segmentations[1] * n0 - + segmentations[2] * n0 * n1) + + segmentations[2] * n0 * n1) # make segment labels consecutive numbers starting at 0 labels = np.unique(segmentation, return_inverse=True)[1] return labels.reshape(image.shape[:2]) diff --git a/skimage/segmentation/_quickshift.pyx b/skimage/segmentation/_quickshift.pyx index a924d72f..57009ad1 100644 --- a/skimage/segmentation/_quickshift.pyx +++ b/skimage/segmentation/_quickshift.pyx @@ -26,30 +26,30 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=Fa Parameters ---------- - image: (width, height, channels) ndarray - Input image - ratio: float, between 0 and 1. + image : (width, height, channels) ndarray + Input image. + ratio : float, between 0 and 1. Balances color-space proximity and image-space proximity. Higher values give more weight to color-space. - kernel_size: float + kernel_size : float Width of Gaussian kernel used in smoothing the - sample density. Higher means less clusters. - max_dist: float + sample density. Higher means fewer clusters. + max_dist : float Cut-off point for data distances. - Higher means less clusters. - return_tree: bool + Higher means fewer clusters. + return_tree : bool Whether to return the full segmentation hierarchy tree and distances. - sigma: float + sigma : float Width for Gaussian smoothing as preprocessing. Zero means no smoothing. - convert2lab: bool + convert2lab : bool Whether the input should be converted to Lab colorspace prior to - segmentation. For this purpose, the input is assumed to be RGB. - random_seed: None or int - Random seed used for breaking ties + segmentation. For this purpose, the input is assumed to be RGB. + random_seed : None or int + Random seed used for breaking ties. Returns ------- - segment_mask: ndarray, [width, height] + segment_mask : (width, height) ndarray Integer mask indicating segment labels. Notes diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 684740d6..a4f37fb2 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -12,24 +12,24 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, Parameters ---------- - image: (width, height, 3) ndarray - Input image + image : (width, height, 3) ndarray + Input image. ratio: float Balances color-space proximity and image-space proximity. Higher values give more weight to color-space. - max_iter: int - maximum number of iterations of k-means - sigma: float + max_iter : int + Maximum number of iterations of k-means. + sigma : float Width of Gaussian smoothing kernel for preprocessing. Zero means no smoothing. - convert2lab: bool + convert2lab : bool Whether the input should be converted to Lab colorspace prior to segmentation. For this purpose, the input is assumed to be RGB. Highly recommended. Returns ------- - segment_mask: ndarray, [width, height] + segment_mask : (width, height) ndarray Integer mask indicating segment labels. Notes @@ -100,7 +100,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, mean_entry = current_mean dist_mean = 0 for c in range(5): - # you would think the compiler can optimize this + # you would think the compiler can optimize the squaring # itself. mine can't (with O2) tmp = current_pixel[0] - mean_entry[0] dist_mean += tmp * tmp diff --git a/skimage/segmentation/felzenszwalb_cy.pyx b/skimage/segmentation/felzenszwalb_cy.pyx index c5f3e705..d2c2e00a 100644 --- a/skimage/segmentation/felzenszwalb_cy.pyx +++ b/skimage/segmentation/felzenszwalb_cy.pyx @@ -14,7 +14,7 @@ def _felzenszwalb_grey(image, double scale=1, sigma=0.8, int min_size=20): """Felzenszwalb's efficient graph based segmentation for a single channel. Produces an oversegmentation of a 2d image using a fast, minimum spanning - tree based clustering on the image grid. + tree based clustering on the image grid. The number of produced segments as well as their size can only be controlled indirectly through ``scale``. Segment size within an image can vary greatly depending on local contrast. @@ -22,11 +22,12 @@ def _felzenszwalb_grey(image, double scale=1, sigma=0.8, int min_size=20): Parameters ---------- image: ndarray - Input image + Input image. scale: float Sets the obervation level. Higher means larger clusters. sigma: float - Width of Gaussian kernel used in preprocessing. + Width of Gaussian smoothing kernel used in preprocessing. + Larger sigma gives smother segment boundaries. min_size: int Minimum component size. Enforced using postprocessing. diff --git a/skimage/segmentation/tests/test_felzenszwalb.py b/skimage/segmentation/tests/test_felzenszwalb.py index fe68c443..ebda2a38 100644 --- a/skimage/segmentation/tests/test_felzenszwalb.py +++ b/skimage/segmentation/tests/test_felzenszwalb.py @@ -1,7 +1,7 @@ import numpy as np from numpy.testing import assert_equal, assert_array_equal from nose.tools import assert_greater -from skimage.segmentation import felzenszwalb_segmentation +from skimage.segmentation import felzenszwalb def test_grey(): @@ -10,7 +10,7 @@ def test_grey(): img[:10, 10:] = 0.2 img[10:, :10] = 0.4 img[10:, 10:] = 0.6 - seg = felzenszwalb_segmentation(img, sigma=0) + seg = felzenszwalb(img, sigma=0) # we expect 4 segments: assert_equal(len(np.unique(seg)), 4) # that mostly respect the 4 regions: @@ -25,7 +25,7 @@ def test_color(): img[:10, :10, 0] = 1 img[10:, :10, 1] = 1 img[10:, 10:, 2] = 1 - seg = felzenszwalb_segmentation(img, sigma=0) + seg = felzenszwalb(img, sigma=0) # we expect 4 segments: assert_equal(len(np.unique(seg)), 4) assert_array_equal(seg[:10, :10], 0) From d9444e76123ff2b3aa122cd988890b09e906d075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 2 Aug 2012 17:12:59 +0200 Subject: [PATCH 265/648] add local binary pattern texture analysis --- skimage/feature/__init__.py | 2 + skimage/feature/_texture.py | 340 ++++++++++++++++++ .../tests/{test_glcm.py => test_texture.py} | 65 +++- 3 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 skimage/feature/_texture.py rename skimage/feature/tests/{test_glcm.py => test_texture.py} (68%) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index eb98945c..003a7b27 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -1,5 +1,7 @@ from ._hog import hog from ._greycomatrix import greycomatrix, greycoprops +from .hog import hog +from ._texture import greycomatrix, greycoprops, local_binary_pattern from .peak import peak_local_max from ._harris import harris from .template import match_template diff --git a/skimage/feature/_texture.py b/skimage/feature/_texture.py new file mode 100644 index 00000000..68232bb4 --- /dev/null +++ b/skimage/feature/_texture.py @@ -0,0 +1,340 @@ +""" +Methods to characterize image textures. +""" + +import math +import numpy as np +from scipy import ndimage + +from ._greycomatrix 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 + +def bit_rotate_right(value, length): + """Cyclic bit shift to the right. + + Parameters + ---------- + value : int + integer value to shift + length : int + number of bits of integer + + """ + return (value >> 1) | ((value & 1) << (length - 1)) + +def local_binary_pattern(image, P, R, method='default'): + """Texture classification using gray scale and rotation invariant LBP (Local + Binary Patterns). + + Parameters + ---------- + image : NxM array + graylevel image + P : int + number of circularly symmetric neighbor set points (quantization of the + angular space) + R : float + radius of circle (spatial resolution of the operator) + method : {'default', 'ror', 'uniform', 'var'} + method to determine the pattern:: + * 'default': original local binary pattern which is gray scale but not + rotation invariant. + * 'ror': extension of default implementation which is gray scale and + rotation invariant. + * 'uniform': improved rotation invariance with uniform patterns and + finer quantization of the angular space which is gray scale and + rotation invariant. + * 'var': rotation invariant variance measures of the contrast of local + image texture which is rotation but not gray scale invariant. + + Returns + ------- + output : NxM array + LBP image + + References + ---------- + Timo Ojala, Matti Pietikainen, Topi Maenpaa. Multiresolution Gray-Scale and + Rotation Invariant Texture Classification with Local Binary Patterns. + http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/\ + Articoliriferimento/LBP.pdf, 2002. + """ + method = method.lower() + # texture weights + weights = 2 ** np.arange(P) + # local position of texture elements + rp = - R * np.sin(2 * math.pi * np.arange(P) / P) + cp = R * np.cos(2 * math.pi * np.arange(P) / P) + coords = np.vstack([rp, cp]) + math.ceil(R) + # maximum size of neighbourhood for filtering + max_size = 2 * math.ceil(R) + 1 + # center index of flattened neightbourhood + center_index = (max_size ** 2 - 1) / 2 + + if method == 'ror': + # allocate array for rotation invariance + rotation_chain = np.zeros(P, dtype='int') + + def compute_lbp(texture): + # subtract value of center pixel + texture -= texture[center_index] + #: get texture elements using bilinear interpolation + texture = texture.reshape(max_size, max_size) + texture = ndimage.map_coordinates(texture, coords, order=1) + + #: signed / thresholded texture + signed = texture.copy() + signed[signed>=0] = 1 + signed[signed<0] = 0 + + if method in ('uniform', 'var'): + #: determine number of 0 - 1 changes + changes = np.sum(np.abs(np.diff(signed))) + + if changes <= 2: + lbp = np.sum(signed) + else: + lbp = P + 1 + + if method == 'var': + lbp /= np.var(texture) + else: + + # method == 'default' + lbp = np.sum(signed * weights) + + if method == 'ror': + #: shift LBP P times to the right and get minimum value + rotation_chain[0] = lbp + for i in xrange(1, P): + rotation_chain[i] = bit_rotate_right(rotation_chain[i-1], P) + lbp = np.min(rotation_chain) + + return lbp + + dtype = 'int' + if method == 'var': + dtype = 'float' + output = np.zeros(image.shape, dtype) + + ndimage.generic_filter(image, compute_lbp, size=(max_size, max_size), + mode='constant', cval=0, output=output) + + return output diff --git a/skimage/feature/tests/test_glcm.py b/skimage/feature/tests/test_texture.py similarity index 68% rename from skimage/feature/tests/test_glcm.py rename to skimage/feature/tests/test_texture.py index da90ec56..02e9e98f 100644 --- a/skimage/feature/tests/test_glcm.py +++ b/skimage/feature/tests/test_texture.py @@ -1,8 +1,10 @@ import numpy as np -from skimage.feature import greycomatrix, greycoprops +from skimage.feature._texture import greycomatrix, greycoprops, \ + local_binary_pattern, bit_rotate_right class TestGLCM(): + def setup(self): self.image = np.array([[0, 0, 1, 1], [0, 0, 1, 1], @@ -140,5 +142,66 @@ class TestGLCM(): 'energy', 'correlation', 'ASM']: greycoprops(result, prop) + +class TestLBP(): + + def setup(self): + self.image = np.array([[255, 6, 255, 0, 141, 0], + [ 48, 250, 204, 166, 223, 63], + [ 8, 0, 159, 50, 255, 30], + [167, 255, 63, 40, 128, 255], + [ 0, 255, 30, 34, 255, 24], + [146, 241, 255, 0, 189, 126]], dtype=np.uint8) + + def test_bit_rotate_right(self): + np.testing.assert_equal(bit_rotate_right(11, 4), 13) + + def test_default(self): + lbp = local_binary_pattern(self.image, 8, 1) + ref = np.array([[ 0, 251, 0, 255, 96, 255], + [143, 0, 20, 153, 64, 56], + [238, 255, 12, 191, 0, 252], + [129, 0, 62, 159, 199, 0], + [255, 4, 255, 175, 0, 254], + [ 3, 5, 0, 255, 4, 24]]) + np.testing.assert_array_equal(lbp, ref) + + def test_ror(self): + lbp = local_binary_pattern(self.image, 8, 1, 'ror') + ref = np.array([[ 0, 127, 0, 255, 3, 255], + [ 31, 0, 5, 51, 1, 7], + [119, 255, 3, 127, 0, 63], + [ 3, 0, 31, 63, 31, 0], + [255, 1, 255, 95, 0, 127], + [ 3, 5, 0, 255, 1, 3]]) + np.testing.assert_array_equal(lbp, ref) + + def test_uniform(self): + lbp = local_binary_pattern(self.image, 8, 1, 'uniform') + ref = np.array([[0, 7, 0, 8, 2, 8], + [5, 0, 9, 9, 1, 3], + [9, 8, 2, 7, 0, 6], + [2, 0, 5, 6, 5, 0], + [8, 1, 8, 9, 0, 7], + [2, 9, 0, 8, 1, 2]]) + np.testing.assert_array_equal(lbp, ref) + + def test_var(self): + lbp = local_binary_pattern(self.image, 8, 1, 'var') + ref = np.array([[0. , 0.00072786, 0. , 0.00115377, + 0.00032355, 0.00224467], + [0.00051758, 0. , 0.0026383 , 0.00163246, + 0.00027414, 0.00041124], + [0.00192834, 0.00130368, 0.00042095, 0.00171894, + 0. , 0.00063726], + [0.00023048, 0. , 0.00082291, 0.00225386, + 0.00076696, 0. ], + [0.00097253, 0.00013236, 0.0009134 , 0.0014467 , + 0. , 0.00082472], + [0.00024701, 0.0012277 , 0. , 0.00109869, + 0.00015445, 0.00035881]]) + np.testing.assert_array_almost_equal(lbp, ref) + + if __name__ == '__main__': np.testing.run_module_suite() From 82868dd41b5d9a76e579f8adf2333e6cdc469e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 2 Aug 2012 17:17:58 +0200 Subject: [PATCH 266/648] apply PEP8 guidelines --- skimage/feature/_texture.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/skimage/feature/_texture.py b/skimage/feature/_texture.py index 68232bb4..fd579958 100644 --- a/skimage/feature/_texture.py +++ b/skimage/feature/_texture.py @@ -182,11 +182,11 @@ def greycoprops(P, prop='contrast'): # create weights for specified property I, J = np.ogrid[0:num_level, 0:num_level] if prop == 'contrast': - weights = (I - J)**2 + weights = (I - J) ** 2 elif prop == 'dissimilarity': weights = np.abs(I - J) elif prop == 'homogeneity': - weights = 1. / (1. + (I - J)**2) + weights = 1. / (1. + (I - J) ** 2) elif prop in ['ASM', 'energy', 'correlation']: pass else: @@ -194,10 +194,10 @@ def greycoprops(P, prop='contrast'): # compute property for each GLCM if prop == 'energy': - asm = np.apply_over_axes(np.sum, (P**2), axes=(0, 1))[0, 0] + 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] + 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)) @@ -205,9 +205,9 @@ def greycoprops(P, prop='contrast'): 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), + 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), + 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] @@ -226,6 +226,7 @@ def greycoprops(P, prop='contrast'): return results + def bit_rotate_right(value, length): """Cyclic bit shift to the right. @@ -239,9 +240,10 @@ def bit_rotate_right(value, length): """ return (value >> 1) | ((value & 1) << (length - 1)) + def local_binary_pattern(image, P, R, method='default'): - """Texture classification using gray scale and rotation invariant LBP (Local - Binary Patterns). + """Texture classification using gray scale and rotation invariant LBP + (Local Binary Patterns). Parameters ---------- @@ -301,8 +303,8 @@ def local_binary_pattern(image, P, R, method='default'): #: signed / thresholded texture signed = texture.copy() - signed[signed>=0] = 1 - signed[signed<0] = 0 + signed[signed >= 0] = 1 + signed[signed < 0] = 0 if method in ('uniform', 'var'): #: determine number of 0 - 1 changes @@ -324,7 +326,8 @@ def local_binary_pattern(image, P, R, method='default'): #: shift LBP P times to the right and get minimum value rotation_chain[0] = lbp for i in xrange(1, P): - rotation_chain[i] = bit_rotate_right(rotation_chain[i-1], P) + rotation_chain[i] = \ + bit_rotate_right(rotation_chain[i - 1], P) lbp = np.min(rotation_chain) return lbp From 6afd8cc2f7dd3831e4766f81ca287faf7ec3ca3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 13 Aug 2012 15:07:43 +0200 Subject: [PATCH 267/648] merge contributors changes --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 5961407d..b89af6ee 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -108,3 +108,4 @@ Adaptive thresholding Implementation of Matlab's `regionprops` Estimation of geometric transformation parameters + Local binary pattern texture classification From fa352bacd41de5c88446d51ee4f52a305b24080a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 3 Aug 2012 13:19:28 +0200 Subject: [PATCH 268/648] fix typos in doc string and comment --- skimage/feature/_texture.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/feature/_texture.py b/skimage/feature/_texture.py index fd579958..1a456199 100644 --- a/skimage/feature/_texture.py +++ b/skimage/feature/_texture.py @@ -250,7 +250,7 @@ def local_binary_pattern(image, P, R, method='default'): image : NxM array graylevel image P : int - number of circularly symmetric neighbor set points (quantization of the + number of circularly symmetric neighbour set points (quantization of the angular space) R : float radius of circle (spatial resolution of the operator) @@ -287,7 +287,7 @@ def local_binary_pattern(image, P, R, method='default'): coords = np.vstack([rp, cp]) + math.ceil(R) # maximum size of neighbourhood for filtering max_size = 2 * math.ceil(R) + 1 - # center index of flattened neightbourhood + # center index of flattened neighbourhood center_index = (max_size ** 2 - 1) / 2 if method == 'ror': @@ -297,17 +297,17 @@ def local_binary_pattern(image, P, R, method='default'): def compute_lbp(texture): # subtract value of center pixel texture -= texture[center_index] - #: get texture elements using bilinear interpolation + # get texture elements using bilinear interpolation texture = texture.reshape(max_size, max_size) texture = ndimage.map_coordinates(texture, coords, order=1) - #: signed / thresholded texture + # signed / thresholded texture signed = texture.copy() signed[signed >= 0] = 1 signed[signed < 0] = 0 if method in ('uniform', 'var'): - #: determine number of 0 - 1 changes + # determine number of 0 - 1 changes changes = np.sum(np.abs(np.diff(signed))) if changes <= 2: @@ -323,7 +323,7 @@ def local_binary_pattern(image, P, R, method='default'): lbp = np.sum(signed * weights) if method == 'ror': - #: shift LBP P times to the right and get minimum value + # shift LBP P times to the right and get minimum value rotation_chain[0] = lbp for i in xrange(1, P): rotation_chain[i] = \ From c9709ca22aa6b80eb76ad9687d9872ddd383eff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 3 Aug 2012 19:46:06 +0200 Subject: [PATCH 269/648] add example script for local binary pattern --- .../applications/plot_local_binary_pattern.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 doc/examples/applications/plot_local_binary_pattern.py diff --git a/doc/examples/applications/plot_local_binary_pattern.py b/doc/examples/applications/plot_local_binary_pattern.py new file mode 100644 index 00000000..1e65d14f --- /dev/null +++ b/doc/examples/applications/plot_local_binary_pattern.py @@ -0,0 +1,89 @@ +""" +=============================================== +Local Binary Pattern for texture classification +=============================================== + +In this example, we will see how to classify textures based on LBP (Local +Binary Pattern). This example uses different rotated textures, taken from +http://sipi.usc.edu/database/database.php?volume=rotate. + +The histogram of the LBP result is a good measure to classify textures. For +simplicity the histogram distributions are then tested against each other using +the Kullback-Leibler-Divergence. + +Preparation +=========== + +First you need to download and extract the texture image set from +http://sipi.usc.edu/database/database.php?volume=rotate. Make sure you change +the path to the extracted images in the script (`IMAGE_FOLDER`). You must run +the `dump_refs` function only once, so the computation is faster. Finally you +can match any of the rotated images against the reference textures. +""" + +import os +import glob +import numpy as np +import pylab +import skimage.feature as ft +from skimage.io import imread + + +IMAGE_FOLDER = 'images' +REF_DUMP_FOLDER = 'refs' +METHOD = 'uniform' +P = 16 +R = 2 + + +def kullback_leibler_divergence(p, q): + p = np.asarray(p) + q = np.asarray(q) + filt = np.logical_and(p != 0, q != 0) + return np.sum(p[filt] * np.log2(p[filt] / q[filt])) + + +def dump_refs(refs): + os.mkdir(REF_DUMP_FOLDER) + file_names = glob.glob('%s/*.000.tiff' % IMAGE_FOLDER) + for file_name in file_names: + name, _ = os.path.splitext(os.path.basename(file_name)) + lbp = ft.local_binary_pattern(ref, P, R, METHOD) + np.save('refs/%s.npy' % name, lbp) + + +def load_refs(): + file_names = glob.glob('refs/*.000.npy') + refs = {} + for file_name in file_names: + name, _ = os.path.splitext(os.path.basename(file_name)) + refs[name] = np.load(file_name) + return refs + + +def match(refs, img): + best_score = 10 + best_name = None + lbp = ft.local_binary_pattern(img, P, R, METHOD) + hist, _ = np.histogram(lbp, normed=True, bins=P+2, range=(0, P+2)) + for name, ref in refs.items(): + ref_hist, _ = np.histogram(ref, normed=True, bins=P+2, range=(0, P+2)) + score = kullback_leibler_divergence(hist, ref_hist) + if score < best_score: + best_score = score + best_name = name + return best_name + + +# compute LBP for each reference image once and dump result, once run this +# should be commented out +dump_refs(refs) + +# match any rotated image against reference textures +refs = load_refs() +img = imread(os.path.join(IMAGE_FOLDER, 'grass.060.tiff')) +print match(refs, img) # grass.000 +img = imread(os.path.join(IMAGE_FOLDER, 'brick.060.tiff')) +print match(refs, img) # brick.000 +img = imread(os.path.join(IMAGE_FOLDER, 'bubbles.060.tiff')) +print match(refs, img) # bubbles.000 From 2efa339cc8814af306e700745e555cef6afcaba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 4 Aug 2012 19:36:37 +0200 Subject: [PATCH 270/648] move local binary pattern example script --- doc/examples/{applications => }/plot_local_binary_pattern.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/examples/{applications => }/plot_local_binary_pattern.py (100%) diff --git a/doc/examples/applications/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py similarity index 100% rename from doc/examples/applications/plot_local_binary_pattern.py rename to doc/examples/plot_local_binary_pattern.py From 234fb68ee724be34a741d8c354bfc3f0d9a62df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 4 Aug 2012 19:52:49 +0200 Subject: [PATCH 271/648] make local binary pattern example script use skimage sample images --- doc/examples/plot_local_binary_pattern.py | 71 +++++++--------------- skimage/data/brick.png | Bin 0 -> 174069 bytes skimage/data/grass.png | Bin 0 -> 203724 bytes skimage/data/rough-wall.png | Bin 0 -> 197789 bytes 4 files changed, 22 insertions(+), 49 deletions(-) create mode 100644 skimage/data/brick.png create mode 100644 skimage/data/grass.png create mode 100644 skimage/data/rough-wall.png diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py index 1e65d14f..9ef262bd 100644 --- a/doc/examples/plot_local_binary_pattern.py +++ b/doc/examples/plot_local_binary_pattern.py @@ -3,34 +3,23 @@ Local Binary Pattern for texture classification =============================================== -In this example, we will see how to classify textures based on LBP (Local -Binary Pattern). This example uses different rotated textures, taken from -http://sipi.usc.edu/database/database.php?volume=rotate. - -The histogram of the LBP result is a good measure to classify textures. For -simplicity the histogram distributions are then tested against each other using -the Kullback-Leibler-Divergence. - -Preparation -=========== - -First you need to download and extract the texture image set from -http://sipi.usc.edu/database/database.php?volume=rotate. Make sure you change -the path to the extracted images in the script (`IMAGE_FOLDER`). You must run -the `dump_refs` function only once, so the computation is faster. Finally you -can match any of the rotated images against the reference textures. +In this example, we will see how to classify textures based on LBP (Local Binary +Pattern). The histogram of the LBP result is a good measure to classify +textures. For simplicity the histogram distributions are then tested against +each other using the Kullback-Leibler-Divergence. """ import os import glob import numpy as np import pylab +import scipy.ndimage as nd import skimage.feature as ft from skimage.io import imread +from skimage import data -IMAGE_FOLDER = 'images' -REF_DUMP_FOLDER = 'refs' +# settings for LBP METHOD = 'uniform' P = 16 R = 2 @@ -43,31 +32,14 @@ def kullback_leibler_divergence(p, q): return np.sum(p[filt] * np.log2(p[filt] / q[filt])) -def dump_refs(refs): - os.mkdir(REF_DUMP_FOLDER) - file_names = glob.glob('%s/*.000.tiff' % IMAGE_FOLDER) - for file_name in file_names: - name, _ = os.path.splitext(os.path.basename(file_name)) - lbp = ft.local_binary_pattern(ref, P, R, METHOD) - np.save('refs/%s.npy' % name, lbp) - - -def load_refs(): - file_names = glob.glob('refs/*.000.npy') - refs = {} - for file_name in file_names: - name, _ = os.path.splitext(os.path.basename(file_name)) - refs[name] = np.load(file_name) - return refs - - def match(refs, img): best_score = 10 best_name = None lbp = ft.local_binary_pattern(img, P, R, METHOD) - hist, _ = np.histogram(lbp, normed=True, bins=P+2, range=(0, P+2)) + hist, _ = np.histogram(lbp, normed=True, bins=P + 2, range=(0, P + 2)) for name, ref in refs.items(): - ref_hist, _ = np.histogram(ref, normed=True, bins=P+2, range=(0, P+2)) + ref_hist, _ = np.histogram(ref, normed=True, bins=P + 2, + range=(0, P + 2)) score = kullback_leibler_divergence(hist, ref_hist) if score < best_score: best_score = score @@ -75,15 +47,16 @@ def match(refs, img): return best_name -# compute LBP for each reference image once and dump result, once run this -# should be commented out -dump_refs(refs) +brick = data.load('brick.png') +grass = data.load('grass.png') +wall = data.load('rough-wall.png') -# match any rotated image against reference textures -refs = load_refs() -img = imread(os.path.join(IMAGE_FOLDER, 'grass.060.tiff')) -print match(refs, img) # grass.000 -img = imread(os.path.join(IMAGE_FOLDER, 'brick.060.tiff')) -print match(refs, img) # brick.000 -img = imread(os.path.join(IMAGE_FOLDER, 'bubbles.060.tiff')) -print match(refs, img) # bubbles.000 +refs = { + 'brick': ft.local_binary_pattern(brick, P, R, METHOD), + 'grass': ft.local_binary_pattern(grass, P, R, METHOD), + 'wall': ft.local_binary_pattern(wall, P, R, METHOD) +} + +print match(refs, nd.rotate(brick, angle=30, reshape=False)) +print match(refs, nd.rotate(brick, angle=70, reshape=False)) +print match(refs, nd.rotate(grass, angle=145, reshape=False)) diff --git a/skimage/data/brick.png b/skimage/data/brick.png new file mode 100644 index 0000000000000000000000000000000000000000..c69e71b2851dfc44952d93a46bfe5a2eb37355d6 GIT binary patch literal 174069 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�H?0 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 QcmMzZ07*qoM6N<$g32Xt(f|Me literal 0 HcmV?d00001 diff --git a/skimage/data/grass.png b/skimage/data/grass.png new file mode 100644 index 0000000000000000000000000000000000000000..16062a35fb483edd22b17e2ccdf42eca1f046e85 GIT binary patch literal 203724 zcmZ^~W3cE@vn{%?mu;JS*|u%lwr$(CZQHhO+s519Irp7=@2h&LO41n_WAyBvRmqRc zgv-f@!a!m|0ssKOh>Hm+0002}3xNQ@{~ZD5ks?X|ognPQ)Exl;ppgEv0|I1Zp#uN_ zE}JWYdJ|JnX~oKEH@|2vb7w<&1p}38cvBN*Hlm353_dn|Yf5!jUOV+{M z_+Qokm-_$2{uiH%_CJOHAC36$F8>$$ugyG=T(ti)Wjv5XaryxO0DJ)ALi|c@fY@EC zu_WRtq$V59Z45>c3e+ZSoSb`9^-%D60C?}Y??OCx`Y!%B^NY*N%6|!o4kT7@PEJfl zXw`x}#;T^Ue5R*o-k0br50&7eCkwS+BM5Kb30s1(a4S_Y@!WAC7E#C zn|{c3e~Ns5@k$5J2On2I`G|XaXvRs;)t`=CJ$HX!HxiEqT_1COx3|;su^LC0tG}Eb zl5rzAUsumkyfR;|j98yEdp%QQ1qpe+2Y%{^q)Op9R2(_ZBi?ryiUkcAc1%Z+rj=5D zr$j^w9JF06IIM}D!%^g^ZCj4_Wp5@>$3@s%ugPtHete^Tt-}|7GvdyF;I%AFU$vR8 zkyb!ydB~o#R<@_sS8!+1I)6i_pIddU3>to~ZN5sSWpsitb4p$DI437`X;%rlEUPAb zx@LbKeuJI_H%zzBlf8cJex`?7yt+lw?d-)oH#jsk2?zA1SqArSLT@@Ml`2!skZgXP zDpW30+%(Cq@>6c=0%n3R34kcJigby*$X5fQ7)fk5iO}}TkbX|!-DJ-!VHTw&DYI&De?*r;YM&`6SuT&=%9TmGC0(z`9nqqK&9vvL1yf2{K6_SSuy#q20OA9lVNtpGBH?4zIv zS^+n(%VwF?N-|?w6gL@7u1cs9@)Uq`zjEz16(Cut&Rdum;Bd`t|1yI<%paByROU%0 z8wEVeKEP?ZNNuk5cEYEdA&$yd6vOYsD>vviwJ{?-FJD{*m$qCIHcm{1PYI%jIdn)c zYajh^Hu$WV<|E~@kzT~lS}uwey0BXgP@Jfgd8V($WuH@x6Sfx7GR5Ckd25Gt6j#bM zmLFD*mnH9AeiV<~r&ps5`}nfU>SspGJ??Z8cbCLx$%(m2w)ZYAMbrPnt!F!+-FviG z)#o1~H!nfXBPj;yH$&s3Qza6&TqpZhRob-}&1$riE?pyzAoDJCZj;7lRsLPh>1@H6 z)QfN}Of^A0hFdj#_j!pXW-VBSz*}DarpI7hmtj)A{h8$5Nsq84hC*FgR9=my^v!?L z*L^cKNb<}b*`48;#4YK+2JYQl?Jl;evzNQ8;Li4Fj_BcfZLrmnA{(`}@%6&~arm&- z_}dAeS}|F7DQa!BfKUxl8iGC{(lx03P=rIf$bccsyuP)iS_pGvxAy6Q@LEqM*+qki zS~6el_3HD^lyM!3)gp47fnBe4^Qo^lJepfzP%j@wJ()fEMj;zT1>Kozy2#e+?!;{x zeyb#sn>Nfa6kCF*v+p~cY*>DE7$%t)5x9Gs3+$?P)ffTyY zz*I^hl?kdG`rx}&(!ChCpVA7mb5Uo|Vj_-H+t}{joJ~rI6p%Lqc`jm5G_7i?Xq4DX zTb2G<56;az{#ik2?|D&`<`GQNw#akJpk8U4>iH==1-RXnCEjvm+&hzYJOy%jaPMu$ zk)vTUU@1x_Ejr-xwHY2|MbQ1we<5(Q^eo_;I`PIhJ=Lb;>~R7_l417X#RE%D!t|cH zh|KJCBH=XegLWy%R3^+J{szs*NG2$En3u?|wuLM-#V}$r9*jY&%?jbb3tJt&%lX4m z*;a_>V`=^3mm#TY^j>}PWzvWeiX_cbHdRbWYI!Nry_Iegr7G;77}sKmUXF3gu#7G0*pSVez^$A?W}y6Je&O1;8D%SCRsJo~^4w>Pv_^SY&$fxvrA)gMg1hO&O^Kochxas`4-D2)Ed;}Bi z21!_um*}iJuPl2c`R>h}dRCazC)D_hIAh2l)02x~QvEpSr&Z=}iW-@X3N0!=J~t-X z>HCMBU>f$S3G=%F(pP%gy7c0Fp&Y47sZLIgX|4sfNPMLPNaVc(7uOokXe5;JuY}uL zD^q#GWU0!reOz|1LrtzVX>f{JA6X*Dsdi_+gb)rAYxYf1xjFgK5o&^Q3z)$*;z`QN ze7VP34e$QYnI%nZKD}`Ds1xlwd~%|?jz|4(J$fbS{pij!KehEOV&m`Yeu8j!Tf3XX z+j)>EuR?Nj?gl-Mg5><_DO7%v*`s^&?}~7_CMLrX7yMRXf5fm<@O>aNT4P70Z#1Il zi7CRcOAd=DJ`?`qkn&TVtP^H*6CYDoRH>Yhgz?pq235)QWm@ zv{$NEfksf0vWRx+q%}crMyFP0582>G(7_SM=k zX5q>Lax<@<&p;&^g(_5PmIg9?$68PqR{Qg?t2SGtN{PgpyC|y8yH2Y1NAV^Y7`F3Y zi>`*H>VZv{mVsI;Dop1qv!|p?e2syY?xSon!-0WH6ZrzL*C(HJhL$HuB)R(N5|el$ zEHdTqqf7ddBN|UaMfOgVUU^meBz>{16;{K(!6GrE%hSBFqFdt5IR*o?-7UNM4E=qw z7!HHeAw(>X>1Bn3%{0BR){fPfcyI9z%+V?7bQIy-KNbX5m{%S+U|@ry-SN-Hs8k** zIvzbwJW~F$&tis4XN$?DxiUVPyie&e#WriZ+h@HaBT~#P?OBnzKliH_no{blLR#?^ z=^rOD=i-j2L#@IaW18??yrazdS*&J=CaqPD0AG^Tr0!toZ-wlW9yikILuCb*ij&jW zBm7RDt>5o-_58xDJ{d514pXu=NK?mFr-|o{`nalSkt#tX!;vGCIQ+^$EmT*pt+Kr& zSgLT!Lovp^dE%E<2VF46$no)i5_1V-EEp*)OtF^ED1)ZywT&5b5$`e8zBm&S;5vw; z8S-q3j=a)rAX4}^`j)~o+@@N~OBkr^R-#dt`&3j_Bj5sg7W8N8Qlz4+)pdfi28kq* z{j#7_HS#NC3>0C~S!|%YaUNe;@zd|w-HbZFGU+{-<#B?^60B|ssq$VF^+XD|W<2ph zdxjEPTmPOKg6&{6x!6Z$pA$a|9ipW1S(^ME)YCv}kXzI_F{WT0iQx@9|J!aIu71-^#-suD^-{Bsoe_OLDzSi@;h=+aIiv#h7Q z&TyVElFmU9!~sZw2Nar{B7q{~#LR}Y(dP;oJnTTZptu|CWxLH{qh^bMQvpM*^jIo_ z;yd(WOx>D|4vuaoZfJ2F?Eb(MZY|Jz4g~T*>jTWA=Hw*2($bM}*kg#$;F8>olso%MxJc2SHz~ zY^{Cypx^h$0B!-JIMAEZ)Y6C^U+oCEYkZ*r=E)mU-`6{RjDQMgUd=v)8nYoUY@90f zRC4}4riu(kfdVVs&QpoZ%(l;gomp=jqiA}kCSi6i?sgN5R(xVV$!Dpe<$_+0!zT$5 zsy;vo(H`iGEykao3*PhCjtstTo-VAMQ_YSi?9D0R?u=&LvA*Z*HinJ#HdFky^FKxm z=gl>sSqS@&v97taoC#2}1IlEmx}kqzFjf7?MC7$r5))k&9pgD1CW=Im)v#zE1=!N- zq8Cm#u(fLycjK4^)|a6cZe4R?aHre3D-w*dG%ln1 zT}f;7ckVsiLwCO>&}hxj?;f$nBd#Mi9R}Ov(`5yg@EwkS!ElJ4$2J_V9f+oy>uw*5 z<&AB30l1$Xa5|0tRMIW@ejmMI+bPy$EIFrIj*hy@qnp%E@ASrwX{U?uCXWW8(BPWp zi|kqxuLXNr9Blu{FRhjs55}dlyF=^tdhzqVT-u#Mv;5R$fSOm5{M7s&HzEvXZP}PM znRCAq7#8S9{4qQRF8)6dg8HTX?+pi9kQbM-YS@+GQgFy_)Oj* zj@n7&XGK^Qw{-E?m*$1oLCS6j@f(tC@Xk#yCrfSb^Tz>W;Nyez8&v}M(?-lIm0Rjw z6bnGzf02TRoo+(TO;T89QNZ&)-u+yL#h%I0j?QqZzgGa#R+mmxKhZFck*>H6EfTG0 zkRbO+o>rL?ulFL!=_X*BV*7G0?VDURJ4h0BjChwd?!OLSYE^P6Yy=GR6?uYZP2s56 zj3x>U5}&7;+JIL_q%59-nt2)ulz^VxNv*A;o-N6Xx~V6=GE2K`p)FD`8}|92K>esK zv>nY7EUCB@2~vsspa)S@H~XcpyYswwWts;|#F}~h@w^EyKY$ufQw0&!_cu(l`N)5nqsd@J~iW*n` z6{l~t5_-|O`^VrnZ#EFMDqQvTs^;^xr4lidbWw`|M2gu}xW(YvHYtpIVg6tBpEpsQ z8itUNH4t%4!KsnKPZ}T`2jpwOsyrsLvkHV>bU}bePE^r(mJ4kxK9WPkAzF4yd=W+u zubH!z5l^Mz;{uFd-O+aB>Q5F95yNM~#;!IMCU8eh+)_K2%WrJF6w5Y`q|_4XU0bWO z1N@Za?7+A{M}Y^UQQHoPmN?c|OlKo4QrKdAup^m<*QK=Lj?1r8*G^6olVdA^u**%!l0gQ&ARxDb z8mK=5aPUflYDvObM%O|yw5yjd`q}UA=1QG;TAu51p#`RK;GLb?B0%ufscdU;UeG#~ ze;#g^47**w7Zq1&7^b9KBSQu8khcd8LtXJEo4d=#1m=9&sHgrM&%nUw7CG$6`^ewL-N`wP5*7^7p%iXpu%PMCiAlBu- z)zpU+^#eihO!Z<}V!cQDAGJpnb~PCkOaoO9sYL6V1U@#>=Z1QH)1dpL<7d(ihRq;? z5Z|0aDhE%&0~lo3w3u#R;o{3S3o<&5gZ4=aUp0aeRHctB1=8fKl1}BnmATC|26q8siJU~X18Mp#0}u%6ne;hr zu*>=Pd&pLaqNJm^>BE8UFf4SS{6TQT?SDqWG?0C`gDkm@&V)faC#eL}52e?6)o=FM zG}3jJ3O$dpzJ#hDp5>Ai5g(<$ScyOS!92=~ER?9!W{lLI0AmU>m?j{(1~V%NB`@Pd zo48k&q1DIFm!I%pTo4L*(RZ_+LZ*FI=e$&5R{s_1VM^@PtG~hXL0_kYy%`<-h1|l| zXj*+cN&PzyXpSVCQFa%*+b7H54UWbdMmra{*DqbCmG}OnU)>sg(h3td2(huwh9|kf zLb;R=)9B6Bx)EYc^$j5RdRf^R&=x6EXh~}TgUnh~L+f9nO>3ljcnKi>)Bz)gxx>ZN zOe3>W`|`ZJbJzI-Qmfpcb*{f&&>+jK1KnZaUiZ{8ci->6;{2pk(=51;fo{AK6ZauI z{$p2BqMRDTuWM5iXPC0jR~yF@ExUl&$L^!5q|0Cp21QwSq65E1C)HDzpE+8tw3xIU z1H3LP;LN~e3b}`9t&a8tDuJQZm}llO z`ipxbx|Ey3wxa%nxN~an7!qub^%S9g^?1)zNKnbO8uV9PM4JURJ^%|; z0H-#m;C6YeMJo5AZ;oW_wbwgfq7F2fF_Aem48c18ioXM*a50huFuP%&cck2QRS%|< zY2ola7i%xOg_}Q$HX(8ZuZGl*mBR7$MWlqu@`(rK!R>ma)xdXeB`doj9%#88f@0*u zBySzI86e8nzlG7FUG8s+^4PVWKs^O)OUX9hxdReAHU9fd>n;B-h4#P>Kh}zAU(wN+ zem)KdiL=GB^=5Y7&h}KTcXQpRrgPR`!+1=o*6_@VM<~0N0e=o|exY&NM6Hv0=gnw_ z=zFCq+kqN?5_VNy>mRL4Y)@X?ou$m$9FgrH5rXdBo8;NOn(azCSG_N$#YDtlGJAIh zDUe5SXJ+7gIJP5g1Sg0f>C3H5$~qCxur=c@M6Pz2TJWF;e3=H2cWn=6jXn4~|) zRiU_(M5iz?^3r%#Jv$yA&izn-yvD*8rb+$$3Lxzsp@85ltIrT7KGF-?8-f;7eIN0kj69t{H!u znI-Y}0H(L@=?|)rYcl#jaUn&z9nFA=WPu#`y>q!v{hac84E#5!B|&yo`C`E02j{`G zlDSkYsEvtz6RlvodNaiYgggbyF-3lK&5nqzTP_VVCpz71@}j6MBpC?r8Y2cPuKst}=cDq3cPj_l+E%fYAgM$(AGzL|utxRiJyuYrI*rXjv&-?1 zsO^Hhpmd2t+I(=I9!bSzXT6+Lsh%*D@TQ#4^#Sw?3%$aoQ7Qx)N~>Y?yQL|V^+Cql zNm%i`A_?Q^2)?ccrWa%ipj7nj>D}RH^)Y=kvpUnMz$H|zT)sfE9Q25HLR7C-J$ww6 z@R=^*EUJLpvo^WtIC>ze+HA?;J~+q3AZSld;=H`RtNiQuTL)m+S4kDPu|w(3cLGLqdCpw>-O_$J+ z#lrD`h^x(ZpbvVf>fuZ6m2TN2XAv+*&HPTo3nhi+(eC#;q>IS3?2T#^zl$qP+zV51 zqS+=bhxK_sr#C~UzKJ8Jdg`ui1jc+MY^1)?2+7` z8r7{Fb_qPyF8)sQCcnnu@{yr|{E^}y)Sgq!;U2ROh#j*!y6Eosv3NOJwM6Ibpha0y zhpG(WH-ox{T0lBH`aC-seS5?nTh$23*93O-ax`xMH-8OmZGAYCUB)}g)tLekjjz0Q z-F{9+^)|g%I5is9#%RV@`W$^@WMiJGn7zam!QXm^U~nOZBf_)bW@qz5VuG=}5_9Y_ zB?>mk&N|Ja;{O2L3+x7j^#SwE$Y@nVF{ZGBPznSr$%T8!*6C)cze6oc|3yO}9LdXV z-r!S7p9ltuJLUhws+;S|gmV7x$yre+JQn!XYb$+0U&PRzgqV}?7nf7u5W?pWas z^@CycZgOoAsh; zyLpy~mUh+tkzAHYJI~(!o#@^NWg6wM2d~6i{CPH;z!Yx*MS1{yRukDyRE7edEv`&g zclY~l^{~w?`9$py%0Ju)i6W! z{dA1$mH_kjuJR*mtDq#!IphgGRSkP-@ZJej0(R<;)W)5l2Ub^%dSimMX8iKA!X2e@ ziEqh=Z30?_f;V8#W|B{o({5)OD)?!>NeZF|kOQv9c&O%%tQkeQ=4pQsN&MXWjWlC2+~r;S zZSeO`V9rTCO6PQkX940j7^D|9mtE7C!e8=S4me}k`UA_@sKK5NyB^uRE8A0>0hyR# zMOg#ng~GJe^_Ygba}F`wPQi5SUkCGsC%PflW9M;IY~%ZF?FG%Srd5Q1EZsQu!jHQE z#bb~x58XszWV~+u=R0s@XfLjwjon~fE^%eQ8)!0aNaz*N0Hcq)x{}2genChnf^jNd zCmAIcHB2&_z{D8$AiiU}5R~8$UUsxbRAg?n%i1@*M*}w4KxFq+_+o&L^bIXo!)0}f20gh~ zKGj+@J)Mvl)^hxoCP6yuKfUeVa;AN|F6~7?3caE&_-{O1gYH3XcD}#H>E20Gb%r4r zU!<8QFAs4Iwzxd)LZO%Z}=nMcyeRj`-uv7Ly z9R&l!*;jWm`%r$U#TLlrzRFnMkv_~NA z0bwyp$%Q3$tPp*o0!;8DEotRyBqbIpXf2;C4HuJ-l>f8WG~{M1_*HzC^zG!n zX)^oZYp_SGp^WEoRaV$XNqN|8K#+IS2Y~K_;RLby%Up_%qMwgmO>y_EmB$Y1_;f{y z)JNycV~u&Y6wMN{Ru+J1QKUX~3Rqvef2gOuIuS5(^*yF*-h&{iv{Y~urp2N0yC zpY9Kx@*q{%4wme0=Q?_rqYEW>@>nrY2sAqYmrgQyal}lxHHF2W-Yp)$5SRzM08dl& zc{_Mu{A2a;3Kn$1j0ikPZWH%fO<{qV*OEcB#}ZZq7gZPf8S)atNr$9XH9h(1#B2xu zY{i?hrdF<{JHD>C9qn6OFlENUA2Cs@VeIX)Fs+SBHh=sJI9S{ZXN#65)T7b_?sLu#{9> zDNHvVx|jCgkhcxO$)^$M@B{B=O4&LYlcdsJ9e&W&@H( zsDQ=)ZYX{OS%t#vvb~f$hfB?kCv>4RxT8Pbkug4e6jBpb5Stw87W(T(2n4SsbYcwI z>RB|48u)ddSD_=w!h_ePz2EzwCW5pUwY+O2c}ujQV8v1mS;W#H$scPnC7Biu2w^jm zDiZ^K>#OQ(p%~D|-*EZ+*<}NA03G;mZew%Rz;RLaPDf9^A>0LfuRhN|aW!bKIslaT zD#1o#%3|lZP<^Cvk}9aey(TxyuRlQDvPa~A3*XBpg@}i>f@tKl?@akdO$n`*iHw78 z#oREuK!M?eVI`cI?|OIip4lT*jeuYwQi>Uk;So`d2xVs9du8Pgc$dDkB5gnh!;JLM zMTq@jpM&s4I{cbFq?r`zcDA!bz&vQjoxpEZzmtx4``uFE9}Co&*Fk)TRv}>GREOw* z$6xOte_-Ap!y#ePf&mml7EwwEp#(w!7Hk)R0@{U+WN82|+W2l~S~z%Sc?aCgjx(p> zGfUD|FgEnW6?IB4j}R~GL@B6*G&G}8FF;=jn({hoy8qzvML&WZ8GOU^0o8_)#Tfz{ zU{r8&x&#XxG{wX<_J*i;iy^D)j+gP1-YM2oS+7wGWQ~1dT&-S?92U_N#WAL{f*m}d z{c5{}5fz1zDO|07XmXlkE@k(_Zt; zeM_iSnV;`Tkb3ZQfDyE*$Lg?w~2`%`Ssqr-{b zaceeD(I%9WPMk3iii=Y;C$3wcb?M+#6g+AuhAXS}XOWA4KZr%M z06^dyG|AEIt<33J71-Y|e{*W-v>&u#rtwp#fy?lOxryEvEdDK^$=4{Vikblh*X;wP z7f{t0rqjdc5B(H)OS{Ik&D2ftc>+g7sm57J8`qcQNhnp<3-YJ3!}h>9-NZmA{@Ol` zG}7@JmGc~t|KmF8;215NDrC?S#wEy@4Tb+@Y|b*l5Je*H2!O>WAm5;WkUnjo`aZ1H zLjT129GpdzgBeQIv7bzOm#KtUW1}gwPrDH7>pv=|q#^73y$ zm14q`9rXp~ATgo`y@m9f4BT`yG~#MO3)LEf1Z=8+KvuXhHnZ(Dgvw2#W~6z#vPH#m zX4dg-miPUq$y}tXC+tVn3NxkhkidGf^dKqWu_8J1a@R3h! zRzkZ?lN=0v6-b2RenPC_@9ocrJvJ{gkKIl2Fk8QPB>oXd0H<4mOmeu++^CZZWwg$@ zH@t|BA}bS!>xK@a`osZS-0CW!IHvQ`IWvuda~7pfl zL>vGiyW;C?$prQ8zE$GzG~fqd{c_-)vvhTUMpdC{i_gMkU_nqjoySwle*0-s9qc{R z0nm?ZW7RJxc1w1ga)I(xZn)&x46@`9yww7rdogDq3|h#5-PMryi;Gw4mxiZSNw?bK zv2z6kPs5!VaY?jwbr1ge=v?PUY}vrr4LelnPYXn^dbbHK-Dy*;Jt=1=pkpYh;! zxGk{)FXa+<2zTqGW8sNBVN54JvY=-QwkX$NR{TM(-9kc07)JxLy1B8H*B8~~MVwL* zAYlU<=awChwP4U)>q>k~n*3q3AC+)x0JGBl0vZtqXiTac#CTz*eE|M3CUVfhOaP@` z(g9=L3iiPdKKu76E2WgAnJcrpqX_l@&1;i)UkJ={5PN1OZ80XtX2>>B_eU4g0yZyk`9tHfO$K5X;LGRR|||` zZ_5wd35;ouaj!Ql{P1E+yRKNQK9;A>zOh`4pdh%tB@YEJAe6p6uilv<^9Ui|Q9}+T zbGuDBP&xW04T+ZoKYn?}tM%hB!uM)TA0;>WSRp%|(2pSSRGJzb7`l;M)$zbu0&0&$ zK#=gO9`vF_gl<)Dnc3>|6SYSUZT=iO2D3va_4JHd2vVSG4zB~^S#I%PnccfZPaZjD z=;rb3d$IMuwzG~ufcp>va}O1)g4tK9ddyL!aYgd9JClb0Q0S=z5%YT!=<{Q0ID-(jX&=rJq2z#ZRVn(OFIEflI7~*|DiF-6>K%VV?0knI&t?|k`i7ijmvkEY#eJ|GrREY7 zzJ3@n*yZ<@P#a$d5Bi5C2}Je6(V?#e9uPLY&&iYb-%srYv!m40t{Ab3Vvl#=(+Jiz zVIDEPH-SNT3NKufQzTd~*Hy>)Z1VV=jE97p=B$n5dIh!-;bcnF^NkJyEkGRjsWvB8 zMc()X>3+|LWw4Bh4@z-R5|v;*Nh4+rzXyExUyysC z`4H4a)V})R6(IackY6-NfM&${{mKoj&w)>oP#xG_9*mW$oqLPNvxTr4+nv`%1G$CB z0%;++U`;4xKD!2V@Ec3qs(8G|9*ldN_XSFD^TBDjriR51xOK6k^8s3vp@=9;(-m7z zK2Gn~xsxvgA8-ys|1)OPPBAQArKY!T7UGA)v{DpZopWOo@ZBsH2m869=fTGwP+RG| z(rY%=r2bUOa)TP*v8T09S#xxY(si0``jKYMW@TTAD=BC^1J*DFZQM>0;rhvs3rivK?o2i zWdu+UzEQYb6s6L_MgzVleT^AQieLdG8!vzhOTr1117z_W4oDjN0fQ*I+fWh0)DO>| zvX41Q8d}8D&npF08B^Acj%~DT{i3ZCHFesz_Omc@Z0xW>@yAwrkX{{+acn$ ziiSnZ{Gi%hT~XE!TJ8x4*VQe^U;=OFQ=alJ^k9vk#n;F zA%4EON+r6msZ@Kb@0B%;;Yo>0tP3x-T z0^8%!i8^;S{F2?RCM%l))IY1w(;UfvU|0t-@;=lM4kq=XN5}y8RDvqFxe3J27d7ad zG=tv8FO?~?=Nd53{^z-o$aPz_`QGUl^L)M7!H*4lNb&m^Q)JWrh8~Nbjz-ETIFTWD zY^QENB1zB6CU@%knR%ulHH7*U7a>4`g$FT|5d6bflSLV^;GR0Ju161kvZVEv&e5Cu zfi-s_@*BW7_b!&#ssnW}zV1(f;Jc&0Q@P(Fa@Xg;)72I-v8<66_+Yy5 zOoAIHVc|1FDT!>Ec(uv6oJ4AYm6X!4^Y;r4hCEKn=#inJ@8nqiI{bI@L2=sO7FdPG zTJ9q+Xh2YulaNd2hx3EC(af}{CwDGc^zA#V^Mt$2Qcf-AJCw(3lWE7htdVgpm8tFD z{oV+i0s}V$x+Bj>%0Ev{gJH|qTS^A;4><2KIw7~#9di9?*2D19+U&~)T%OIJ9{b=F;tzxpPv%D#9kX2-m zFR;;c^=S^GpbML5hXVF+E3SeDGJ-QVDc7k!%kzu8GkH_KepwXAa|#u@O>^djP>y&Z zbOme~R8wmf_U|8!rap4}M7tQ&C;#GpW?>KW$aG+K|%pr&pByNVyDPSI#A?)-v&(uKbm8e zP(^`N&|{25decK)x@GEASQpToL+QsVWMiITda4I{WH%zm9{;8c*OH;J+DQ@h2!wMG zH~_`3FlaF#z%v{h-$6jwe3YW!xvTjG(vPhs7su8^r@5;qI1@~Y@t?ev6?spA6ubEi)OvpTD zuXzdg!S_27H|c$0rcDC@j%PkAHlj`FikrLA8@O_5p-a|Sw!KpaK;62W+Gy0Kkc6MW zzDCbkJotM4E^nCGyT>_KYXTW%?;)LlP*wY>7OV7;qO%RPxwmiWaC#rVcN&q5C}wd& z(7PtK?{;sL`1dNM1?xZ-(-6SbKi%~XgMTni+dFU$fPFwch>y7!Qv<1i^P^b!R=;;B z`7(Orj;-y;!wWM+I_hlcX>s$tdgbT1e|SmL0UxDagRI+aU%C)_~9Xyfn%tS ztxBW6`?@~BE;fshga-x9j_Z>gXgBvwz3Jto7v=&EzoKxMW%kbH($#RmRmgP31c(q= zE!iPG;nFXff2syj0?FFlS)-UyYX%+W0zFpETE?rNL_6|hjq#5bU>LvXAi2{oEJOTGO6HO zUly-r#S3=VV?j0N#lFYZm8#+vHY#F~AnRwJF3<7jMn}Fg=wF~9UNm&N52ytGzz`w0 zIItTYffA^Z>@bl_@l>}UU?&~G>B=(wbXOrjhx^ZNlV*lb^k^G|A;l44?mi*%_>7)P zo>L;{j<3))uMJ_54kV#HvVpk{jhe2`pm3GGU?f1$;8Kv`{(-oQ5A~|v%Offa0@MW$ zL*FogLU10C+8o{g=Ze8Y%PawAj=})+_-RNKdg1L_ak%tN3l>IU z%O8rqJ;`$GXAd<>3RVoZ8wTT-HZ252j-+AlxyuP$xVjQ1Giv-xZv#?1Q|{}I`~4xn zedLpwM!%ojjwRQwp!9DT(Oyj-ofM26fLBP-(%=LE1PbtGSH48AhF!7Zm4v4-qF&u8 z1}bv8Uu)wlfyc^#%eF3<^?Ak9pk|Mn3B%gdJS>{qPXK!%^Qb7Oa+cRJ$6l>X=5;RP9Gk$ZLi|T zVw@kW3uTmTJ(zDq99hTaKz86cU9058=o;VjfRrUHPTx%Wn}yvbybrhX<)4?AAWmkV z4qkVy2gg>G*+4rRnVny?3|U!D_vp#GkBNY2hrv$#-VApP`*4Y1`qZ-2wweI;L<$v& zq7AH2^u!W`5M}#(#hd(bJ!tit>wM?OkvM$m4%5CE?S{1LES%K11eT83!SPx2=#Uz6 z4=Fu=u*L^ig-guEG^FhsX>BJMn)18yRqr!sE;UBmO$ca)Z2M4fP(B~VVb7o3Ge%X}I! z+gnCkEnri^POT=S(&OUuk`v$;L?11=22o=IK*qx_@<6Roe=JTx)~q@s=#x?d2knxW zpu*iq+mG`BJ0GCC-r`7r2c1Fib=xXUjzMc#X}TlfN7-VBLl@;}#dNZ3c}8I*A%<$0 zK|M(u$7`=ITzGUu{`BqpIfLd<#qN%u4l?220M(F&)P@a*VB05>bHw;f=`WjX?m?YE z;xLqRrH5l+!&Q>P)eIDCw~ZDB+K^`OZq!n|V3g#)t45iE?B1Tgr`jIuu#((pw>?=v zJ<`tY1R%hC!V%O8MI;sWfyhQhQfCOSsXcv)R0jsRXqHvg&q!leolvRNYPg~DTVMxH z%HJU2n`0!ni?9*C|64C#)7?bVNB))6cNYE#+PK17foh#S1)gdRo^ag;1MowNXuvnX zvcX3*z|39^i!O3M6r)2cX-BrkI^{Cm_C#(@|DJPZKLv{-@{;0kVV^Gn+6YHP@qyET zyZRej_-c_t#ohSW@=D`AP#W;-NB(YkKOQsbUkg!CHFY3Ag;x)QS;vkfUNo9(19P-nc8x zx?)#6#2@Duf1L59skB47RDCGA;(EY6(Cv zrSnLHnP8Nzv_(0i(lP_9+7$Z{`@?pY)@;SAb%m5-x;92-p_(FwgpAE6*oLe31AGh0 zeRp)l$JeEzF}`LO9wl;-=F!=FCfnSjVdVPh;!U%E@hibE%u{VHhMqp+F^~ChX|iB?NR}}W~wv7lOwe;8zn6!A<1~v zS~m=$nzj-AWhF7;?%}AYl9}F4JNb4;=Qjo2!3*mweV12ld}PX)_s+We!Ia0tt5QaM z;zo6jCx>m#lR&c~CG}+G%P~Nxx4a8Hg3)WfZ-h2^JNL)G&M%VsP;!=Y601DOWuij- zH!|7HQC29_BGJRJccRE!UiH+t6r!8x!LQUr^SH9Y8I`x9gK;XtVK`;c$5N7?A^9M! z+^~k9nY}_+=rJe)7`WEkSh6AN?I@5-b!S1Ct5A$j>1ZL?*Oa2OYK!b`^?mqHlv635 zNmrxa;o69fE*(xVU{Pu~9L7sG!yB1R>T?;LxdzbWd}y0Wy%Q3?m4R+H)L{a6Krm(RxcKZ^UE|p!ZFO z`nVBDeaK9*JIm-`x-mHMh@lKN6G|DFqJlXS?*WuWATtbKLcm)P5Y9FK4*)7a)xYUd z#))i8tK|Q;tc895x;>hAxWp5#$UrR1P_U(9S2#k#zQnx9#ZIT9DZ9eNb~dnNMuN(B ztIKSO;rcms6?g4qB>!FwqS3#HW= z?3I!?yVCZb5LMn|Zc^~JwGTiWA0$Pz>HxkZaPFIg1o6wrqR2z&+NyF`H0+Ug)R}I~?A4#i z|4!RI(Mte-bFW~UvACSpNf}(suaD`hh}K8%{74+9=?lz^qc~$Y?RV=FmXcbq9OX1_ zn6=`)Djcfv%}KE0q+xB+vCu*zoO0CL*aYaWmAW*X6OHU7-sCf<7gSt0e00ubuMJyR zgdXB8YjtJB{>6n^zBCCxvv6<(8v)$xa^~#w3N=Vt+a;P~pniA~X&?G>&yw2-Pl{7a z=U2hoeQ?}q^J4OP^69xbsfN^`^4xCuanOP4tv^Ch>O130yK9Gh^3}Niu$Op)?u%nT z20e0eF;3*WEgpR2T`kUD+ubAU1a?v(MX2L`>b<>13=abCh~kpx5&1hWlZyG?=&Tx^ zCWGQaLDa@4fb*&TRAJKZkDc#M56H0xmf3R-ClF5*jMrzoiGwESpU!j2ql&{@g^nF~&n~7lxN+-V)kRJ1oG` z#;86t0Tly`1SB(Qk1UoZHb-cqFr6U9@2 z4qmf3+-71`p6Fj1OT4=ei)Dbbt~O(~dERD-b&dyb4kW_%oiY$Q9b;fQUF6;6_vW{M z_)k%C=Fl_~D3*!M{XJ%Qj#>vnPudC|cA@GP1-tjRlNh^J@&32r|9P;=Ievx=N^6hI zsy1n+^eg+twFFmEgXjId|6Av~O@0V)2=1*!Q50HvJd>QT+Hw{k(L}Y@eCzNI?r4reL6GqVWbSfS6T-r4wJh+eFD2U zFHw_4Y}`~?@`=X1|)Gi>8A5X`)%n}&0vqsSPVTO(zf7EkAEabe7;doj`}nps2F@J}F} z4=iDIIloeekEN?5NoFSgb;(Yg0K7$R(+G?&+zY0m_O&+4QcYVGFY>Q>VK`vFteEO@wgRc*GB zL~%5|`JB138R5f)wbE7@Pf=+DUaL{w%uz3jIE9sKuA7)OM}Hgo}}iae(Aw_O*A*4>V|* zHBHn(bh9!3>!VOFv1zO(-yhrWn2vI7TMbQIugY2o?j?<2bsPy*Kh+54$DPD#ml6xF~1dn4u*Tj(!IZajDCxWO--@jQ_*}X zt55&>85n1pgx-Fd)N+?XZ$=7dG@39~;~MdQhFwMbo`F^)U0oueNlpVga|yg6=LXHy zSded(6J)`4xo;}7MR{PX#J?WX&Kqp>`Ug{eZZ9muRsMcR=fSS78PXyNZM#5by10C1 z+m6LSlqlPCgKja(ghWs^45-;z$;CdS*$*qO_F~}7hSodB5*%ih`^+;3trPDVqlQcK z_uetK0_8C_eI+A2R z^#sjoEU+*$pquu}+}3LJS)CtM?G6bI*NP-2S;|Gsy4dgBtT|>@qSbs()S|Rudq_Kg zxpMgzK9=xf|Jqug&gfcMnu*%YtJ@jus`AQCvRplIf+qS%+#xdJKw2=oHT>uI}!X{Eox`>(k>iWBfL4Vq`k~SP)id-K6wboF4Br)nUU)ifp zaXe$%W3INR!|sI12;>*Ds~TlyCaxce$W=9!<7x2<_91(UzCEWrkX9yv&s2skq| zo4zcD93f?=i-A77lc008${Tc8Wv>SY28Q;^Y7rWM5{0)GrCaXbY@bh=bN68XuuVfq zm5)cQ^gv(f+OcNfK%H)?CeH+a{h&4Ux0StzA^}$kDJw9>!EQZyJb3Ac*XrY>*`d>s zQPi^^k#oJ@E7HI%jvtQYI_!YP9zpAr7m~5h8ZCMi7g*LhgUOYvTjM7fc0wl}G-vj7 z7>iQdD+d{7U5-2cSufiHoG#e2C8o{ez)9QojG-GlDH`i3p4jCU-jN`L=8maerg?9JfeFt&EYi2w_mp>uOXf6GO-&plJn@j7T4;klq=CA*nAZ zaU7!)uH2t@3=>&z9cB27OhM!+le<<(=6w7*)&`J?qS`1)weMZN2Rjc+!L>Tx!lIrgL&7M_3UQ#Q$mQy9p%aIx@P2(0UOvJFvG$=Tc{n{71KV zDaDmnZz#=-{-}=YzvPo-VAl&}^-wGUAThpKN_(bO*zp<%Y6|itwBGm0DV>f^Y99^C zJ8jd?g;Ob=Ds8ELhG}4dA>ZVyx8`%=m}6SCG7+|g#zu}NKzXXMaA|msMeOlvj>TF6 zGTefInAjRFD4tbFC)aO`A3q%JFji#N-q`>JSYZpFFsY^h8KgzjNBVJ88lQVAw3YY~o1ErEr3+)7v8vjgmu^^(0z`a;JS z+sPv^m)2Ch5t>Dk;zy_RPNXA8m7wO}19fKRd;G-hdAFyxCrULjcXnlEV#2d?TQpBi zY7RVjV6>sD%y5UA7lgu3Z;m<>uvU0254&)bbsA47z>&Rb zL}fi%j5Mr3Y9{QTwb`IV?Wj9$n|O^;T->QIN3)fUt#ElTdNeNc09M0L&nMI8AKY2L ze=1iuK5!w_P!&IZBRB0Vhty^y{%SZ1rWpYayk6|lD5fI!$S_z4m%)>ATg)+QbrCCY zQ4-6pR_Ud&yM<+g=eO^D(N3^0s_%{nWoqeb>|-g)UG4ibIi=c7GIhK(|4Y=snJ^Yp z>xx0pb|sMwP#>nsK|6lfh_&Jac=7b2QyTR_ctMwNaWV24dd`mkomg?exTp9)HxH6lRBk-{2te|h#ET-mekRdC z9dnq-Q&ZSf-NK4rKvS7&w3!03>R7SY+%@shkS<`eH4}Z`>eYRx;ei>^x}tw-ZZ3_- zJynq+;ml*R0GT5XuqoBTW8jB(Z%SKh1!ey!R8BG-HBvyKRlhg6wOELhgq|+3d-@ zu!N2Pu|Sfs%D~14sEj$q9A5usg>O9u_s_B@4>n5=yonb*|y<7tI4+3Ci7tkF4U)H!dI!{NHwI%R-QClzMt8p^4)VewL+7+ z0nN*SIuzzhuR$S2B{XuYB36yyp|+hIHP`9ONuR%Q#OZ3Zn7tf#wtD%-7r%(@Q7&-D zl*-#*EcA_xAA@mT)!yE`npnDq6b^;2m~PIxPA@p^Dps}Wo`3_Hch0o-Vv7uq_zZPi z6#Ec4$LQC$+IP{!A+QIwM-Fgb%QqPsOzG9K`-HZQYN%~RvsCeHAwYMFRNs{=DWCHc zAAe?x`;fcAEVwV%+qm!UF2axCw9sgH^jhPqIK99*6xUg6uuc*f{IcLn7|Jj>90jMFUm1t~ zjG5WUe>es;sZ7XLO2)(nb??4ZK?u=wA2mj73*sJBXyeqa7lwqYQt+@K=@2|6X^w*x z$jkQ-;wvPjxztJk`m7`toEyJh|9p@a_0e(KvB>FPg@YT{NY9EXN~dAV4TGSR$^L?z zgR=Tu%Of?#0vqr+GkJm`yNh^1wf$HX66fyC`&TxZR@mp!Wmnm-4eIL&<6RsWtK$~t zd#;*0_G4HD96x!{iJ_@45{8c$g>1{Rc02eBr9Zp!@{i$2)fps(YwjaZ)G?qC_m!Wo z3+#^QId@beE1t{)55e~$b0BOqhl@{ztfBMX_x3mjGmT`WPTHv?wJwu_kj2dj}fm5ilf$&Zf+9i_vzUUq6Fx`AL! zE_?%LUtDo+3Lu}s`ovk6Iz5APX=xTILI!b?r^a<+X!aH@OYWeX^VvVvn-2Zil8=Pp z{`5U!qk>Z>B-vu!_9$_zG}b-)9YpG=V&5VAx=ULL zk5P3k$>3IS#`iuI*1*&jUz8623NwX-8b+pis^Lv@p5qj8RtD-0D`nCo9@Qcnjtb5- z9IqlnvSKnrO7IatISriT;lsAZUiu`kY2fa1UO55NjqHiG;#BTubLjqhUTMW&Keg~4 zuo4nbp=*mI`4IcxypH<^tv6j1Ct3X?d(Ko%&-#57KMD`Te$hiB&Z^yYdjPo*2o? z_C@^EYM1}p`2Tk61V+dOZ8!T5sgRH4Ow{RRjO0f1;2hPtyX zr#^KI^8^wDm~-RNrIMtVx+;HcSccs@@B#{pmn5ZdHsVF{yk1^9sV<*57Pak(d1PnS zdUwCID+bEZ){rmzQD5nEpJ0qMb=F0dCzs$x(a9o7N5_xqoiws(*v3L(J_UgBiB_kx?QuA zM+t;d+pWeld`Lvpf(fOC7STR3FS=jx`t(O8XXD9HkKuaNZaN-PFEiUtP6Hh?e}jFD zpT5EM1;?K>mPJH)*WILeudPtq-=IAU0vCa*mV&anj#5w`@F^lVb6~phlqV_2gtuWB zh!5VdBgJN@=s3_hflN44=cBkMfrc#_m_YlYxqs5(-KYakX20(lu>et6L|6@l|IMTQ19FGyr?4imiAmzI5q zfJ>~hMI0$)(NS9I%~JU1jWR{ttHy-I8SlIHn0}-=y`20FVP8j}*Ujpu$DMq6^Q9E{ zGwG){1GCJM+i3wzvi_I2-*-8{?oMPv6#8J2P+lGhr(;8KG^LPKDtLE0#JOfPN4W` z3p56#U1RKNGREp;`6me~27H=`83lSHQ1K|Z9hR2(xdawmH;Q-6D0A~OPceMe5&ljRu9J8QROnN zr4&$|#T?Gfym_R|v9HFr`lmHpveh_3uV)UKBM_W6LEw71Fcv#G2pNsE>d9*%b{>VE zwW^#Zb9(>cKR0vFWq;go~uL}pGee;0KOcZw5&%58wtb} z!oqn?HSog5lrY z0DcyPsCQ6Bw5|N^55Io>nk1|KJ~KSx=twg#cx?o67DTniWOEfhL3a<-pV)E$Q{-kN z(y0xBOj*vgn-Yf5)9hHC!zDYYyVB`8+gZ4nvNb(+&|kl{Og?Zs2O^KiqZ3BsCR0a6 zH(IO^cF0d#0c26_U*@|87By=gnE{8NLE;k5p+}A`T{hPIaYu(h#Axt7`W%+#%~bkQ zI)U~nT!@?dJ>uNOGmbvlel2|^9oO!N+>Mc}-mvKbEa;(b22Lo#|2zV1*%4&g4H&>p zM}mXJXKuhsb8YQ8G3Fx}kjTuotqMhwY)qqB0M6B5aGq!J96IzPjyujMZ(qTp{ONGByK^tY7{@9To4cUH7h3h7Dz7gf- ztk*yMUyXbmDl}orVkxGH`5ouNRoE?k9el5u5cWd z%BY+iP)G_3{E6{I+2?iQl(BhTu#V1m-C@{@6B8tU3B^R73*TkmzO#Pb>ACPS(MfVV z&kMy{vWMRVYVZ`+^<~oq*KdCgEB)zpgVEr`PYJ;CL)XrfrLPYaL6%`?gXlOKfm{^i z9tuE)EOsYr@^8#=ZDbfSE|TEwN2ty$^rgkaci$Bxt1WBzqO;(`X&mj3Bpr7JR*6FP zZl|#Am8)TbUoBoOAs(iN8Hkp7dujgtk+c}ywN9gZwjANy#LWmK2hgZtyj>g2@uubF zs~Y+O2i2$cQfWz?XwB)s7JhE}(3(W<2nt)@&Xsg1Cvw&rhV+??RlTU8i?@wrc45R$ zLrz$nF|vWXfg3;{Q>a$Lw%f(BjO>?9uNp?}@d*q7hsCM@PG_ z*qtGU94==+GuoPA=DF+V`RQVEj?vKdUrYT13%SL_JP9AC<9_s_Hom7U$l^U=xrc;s zws8+_mcYBfzpO~#e<;aVnoWZ&!~q%AR}9(U2FqI@D`GEr{GYjeWzW**oOxkDh+{iy zb&GmLeS=h2kLCKTrztLYG5hs(kgNM>`!By;7#{{ne*O|nGwXytetP#esBM!XgH?q4=b;kyGl_y8!tF>`4$r9p2)8uAekf0es#PyyDIv7g z3McaYgw2?WP3p^+NX#{f_P!9V;6%F52|OjK7+2l=v#q_olbijCo86iR$s?|7T<pMR@&H*EFbZPS<@gLXzR#sTA`i6qZbNDboP>`y!y zA%yFr)YU(9%A&`S{ahMa=a2~k+T|Lw>gZ^}uple<5jKlPCn5Hl8TA+Pv|y zFd~p^uLr8q7zsH-Ut*uU$V4W?0kc4tEr%aB68bJ7ag}WHNC?e%G1~lFh@x~Dj|dyF z{NZVjn}!%MUc4?Hsp0&so7Ulj2EJl+aHW{U4;?etC>`rzJ^(x1mN*yX5R1^(l7TxEZKklfgV|8nQTO;Nq04f%!5MHTONcy296outy|DIKmI zq862L#gp?rKq-?F|M{a@R(gT&>qFEC-S|#iHm9rv&ohO~fE~C6bwd3}{fu)Y%yuu= ze?Wb{lVi*wfbDKgG8WL3vPiG{PP?5~>3DX60Xz-~J3ON>lLw~G)|AzHE?Oi;N@eU^ zD9Fcqydt<`2n{UvXx*W~TQS8h&e1^}EltgfPKFM%&=Q&vF|Wm>?7wZAC)AV#N#p_7 zv_Jt!1dg3)Mz@l7t>uqrkK27op@CwZFz!J8v>C45iTdTp{LQvTo$+eCuj-VI;hjf5 zmU^D1piO5^X1<^|wBOo! z3DFSfYW@^Xn6+pQ(<>5{a4rG{>h^&qmHylB7~`D@^eJuf+14>7;>_dwEV4DO<=m#W zrlXbd2ko(QUkg|E`q=C)G<|Ez|Htysa@e^kf~BnH%>|W%y*}fHfOq2`z87QyNXD(X z{05DVl15mYK3mv0s{A8<*X05)q0z8Tl}w~E;<4-(36li+Vx6AlhX=3E@qvkQ90E3- z^{n`}UwYvbsoHPaN6_R5;z$ytD`tU#SRg+kp_HwOp0tQh)Zj?*pM%D1%4PW+n{4U$ z85q8e>QigcjN^6L9@&77CL@y3*p&cVn!1HXS%VfEfDV#2iPd03D@B#ri4)l>WXp1| z^6zjBk|0P=%R-&eO!&r4*NdC1eoWPPeA6E60U6J1sdGeGX7n*w7?*t*G4g`8rT}IS z?A4>Yc=h&@f@DI5aSrqT6JT}K);P>52M964+`X%97ly91 zE|Z0}C031<9=?)zgc#IX*ei0y>@MZhT&`+0tVINk+A%zoxKbeCLBT}fC;u3;DDT|VgKvy_53#-0B%C3@? z2M5J8=+xp6Su6iYhJyuW$bqD(69UCA3C8Kd-qeOs?M`Z1)BqGlp7VsN5CSD}Ne<#n zfvsk(yBE;8(O9ae*-IyTNrE?R20jZP>dOYBmAKbE@?T~6-SxsWo{1v->CgsWy~sYV zC)CT;#q6zr7$)6=aYby!=~!RT&RNhD#-2Bw_@mi3PY;EQK!}~DHCi|)*kYkSlC*yN za{p6wJ z#Quid=9g?KFud>zL%A}LdD*It=N0P-)3WEPnIXGug%D-|H?EAhco9UfBq*^`nk4Ak zA*?SX;%wx>D9T%q5B%kBs474G&Lhz(5?NrFAf4GvW~CO;joPu3d2tSNt;&33G{i9J zwFle~L8#V?*X7hfvUh)^&f8ziR)gFru3BFRfNe8_cY zzo?LgFE-F)09u`T7CVwymE*nG*XUJq|ePV`3FcQ|NU zwbow{XS#8z#qly@W*fUiDk-fa5u|kUEkag^NDPxR755uv-nx8vyz@HyN&erpWZ#r7 zgW6S+CaIp)>U7~n(goeiBEBydb5z*ZLHb1U>}c}|W)c6NfMRf?vtVinTk>8Bxzk}O z%JYu;M$>S=sw#~Q?87)QdTnN5G*7K*=Onpii%)xpD}0EzceCt8t3(qwT%`iqc%-t#e{uEH{$w{-B+BtUEp7xGnOkAXXJ)03OcRASn?Y zhnV<=QQ#n?#tK0ccEaET*m}zJ8wH`utJ^wxAb~FMGG57cdJ%{Onu(v*x|2MQ-nvG2 zv|>0{sX&nUPd~dV3|VrGMe#2$l3bZ^Y?aY$rrAfZq6E>_y*UsV80v=@SS)&?O^H7nGn5kI_!>&YJk3C*MA>`=KuxWs3 zHO1)dAGBOR;FT|nP(f61Ngwjz3+?clUEH@JRR{|x@fYW4LJFti=Do9`SQc@>wHZSd zoYzYJtSq+eep4N$76A{IO}_eB_%bvO@u9@TH8M$EmR>@-)gydEkjUURvXs*TT{)6u zlnru~knd#YPlYswtZPO0A41TpxPCl4N95nHh`%-h1~5sIRN1678EkiE1a(a$zXWS& zg+e(COKo(3p|25y-2n^7YCaj*FP)C8yN0pFFvO@9z z?0tKY%g@yE7-s{!39x-{>qEHPC>4aJ8`ONUsC#c_Uy|BVpSSL1+nC<&3_x(@fw-u;va*EF463zDJBX;ZUw4k zzy+zkNb8%q)OJPTd0cBtpc+fC7f{_qgKnXdJjn(RTF#QG5*L$5qf#|9rQ-5K|5C8VF zV6lqed;9m?K8^~Dk=iDw+^BTY3oeWvzAzv3iHZN&8=06Z=C)g;3Erp<_qN|)7!;*= zzf=IdX8FE*Ox&3zKT9>$=(=CZ6{qVVhdnZ?yYQuc8Ty<|*A${r))5?Vy;NDG03x|@ z2kqVUV?k#NX~P+FxSoTeSP&Y{g>WL+4=(O72|y@=B;db<#i_X3lorq)x1hver?J=U-vrUB3N7GCuP*iBbV^O_TB(lYD$?Z!Xfyd**?SgHtB~~ZqiY~TnAPcrR4wa-grW%d3z4_f z-B`$7OD=9-W#ndp9uzAw&qRTkqBR?T$|`_CCk45xHb(i=ksW*DAChBDgO2RE2_W;2 zgD}e7`SjAR%ovtBSzBVAjh7Q3``dv^xV^kV+SWZR@d&_Bd0z3P7yU?oDrQD6$9)Z1 zMLnr_qt_bx6LvLaH->dfG996xAm6Qx%_=z8lxG2Ym~9yA7{y}DHmrX(XnkABgi6n7^LUUid*51V*AOxhi z-%R$YKr}hOc#2TlnC|W+&us}ejj`wneNfpxdCSCkum5oH!R1VZy;TV{US4WNCNaW% zAD(P)*@k(QOaKX~V;#G)cPi1kRaN?|TYKorjrj;`sw;tZ5|ULcY|o_F5YJO8Y%o+Y z?V%3d``TIsXJ>?v6zV%Jbaw6rZcgwIISl^EEsuFf(IQEpd9y*0_#OwiWlF4 z^iWNwUHlPOsYjw08e-}bvk3FpkLo0oV?bWSEV$0@Pe1ieOH=|tarIL8OjiBuN;)Ea zlI2T)f0s4c({w^IPUgX%5XGExTHu6%R2r9-52f6h*1JQMS&l&(Kz~KjS#FPU*dvGT z{9aMC#cpgC_d;0ATXdEDH$C|YD8`7{B<8)=_cF0}%;8VuhVUhgHQpmFYR6& z{J-W`!gJIa8K&7}FO8zeU9k>uj#~?<>y|Piblwj~n)_a=VuC!2_DrVMCalm8wLTft z{Jf1NfyHe?<*CKKXTJbYFd>gBgYEt3l^S_dd-7_Wj$LIRRWP!)iD+f2gI+@b zNJ!O*P>3Nr{x{aF7fWpXUHkr5_iHXNUi;BX{uB5V9(f0#*Am%`h@2GzK;{9+T1fZ^ zPy)-Y11tsmzDkfn=Ug9>K!(eaNb>xbue@bQ9u^iQBKkO9pwratc#E87AV(I)c3V(% zazyg|lfPYGPwC3keur?}!7*c)5p>iBXO42o--v_B-Et8wilcYyD#FWws3S8|1jLDP z0iM$KV2E~s;J3p5<1RH;UL;ymhO)+_PG3Uw{rwwSuq3o;T(UF~;95=bVrff+y&3lp`|DtD+i- zVrIZ)DFZgIy;v@#?FcY3w&c0@q<#I?#bmb&F}yg6n0~)b>vn>v;N9vPR43S+)?&zY zB+7t*C#4CAkC6b-q5Ht!PZ*KNR=G|1(IBu6cD7T+Uq_m1VrDLwy$uW;Cc^m9o&80V zo=aK!ELxE`*H-uWPA7NQ%|r=V;f|V}sL?BV_fV0pp)j~(kWDaSi5hWMZ=%5J%V6@_ z{JZVXeo!2wBa-t~Wj!f2oYuank0jNw3}jI?PM*lv>7|sDPVN^Q3T_p4y z(p@$KhK(E7J2531B+dYQjTcweD6{eMGMTy;p*)Ip+{GRtaUJ*wx|pEip5qM))S`7}UTa67YeF2>$)4Pa_G#-pd5gorJL_(@wy=aENL-qkdb75wsko3rc}W zhsI51mcT#bnCl6IV9NVV&YhD%0V0m@80%*v&s#9aDz}9SDjV=YWd+cb%<24d2o6dg=ibkxXl956;!@iS1j`y6;wU3`8qu zHT}>mZf9COiWGV=bt{=u;qn_}qx?1G_|a!|@zWVa)UF0WKE7z)4zo*L=$^kMjgnMK zXoRo;kfjMN8(APGs)+$XfqYgQUA6~!sR`()u2Bn>N#?W{BUnW_tVM)cIP{Z-WJ*xB zq@o{$i3GRn?;3X9wnCdwWtqgt;wF-e6rNG>4{^ zU^8ZfTgt!S{B6-Px?JvD6Dpk1R=gpwuN-TJeMQ2YPtb7F|GC~KWi=hkJXZVBeDAo* z_fiw!VI#*->YgdPbR7nXDc(paD{P}nFZ`Kb-9J22i&>_D*P|v*18SK3Mzd~6L6#?% zzj|Z*5VlYN66agTO+nEq%kiPpQP%Q1m&7xz)YtLKLRc>$zY4k+jIJk#hhlo+|F9!cSUOy&D0Wod#A|8M#)}smEapot&gTt*qSXzY<|{3q=tUk&7{m}0R_K8RinvvsvtKg8b#+ZS#K;D%7@+zh4~L(BEnK4Th%aVQ z(o9{+C*h~qtzVEaK&7tGzS^>>A5X?rI`)F$qhbH@kC)khdJx8g5LB~~x&Dr^vbP?- z{e$)j*k{c2zpK237}y7pM4hWL!?!Z(iv4^ckWOn~54rRiPMe=GoJa z`5mbaCRPlQF`iW8O9jp*EG`b%Hvt>30e+*5&k*s!6+(#WTl-Ut&qNw0?)dqG;1mSJ zl45pg(NJQ^5(%e_3bV0xs>7nIVLdH9f+5Ar(q;`RY@buQfg)PtWVRMWL89ZzqBE`D z55(5VZ7}SWwfBvHI3X%M$=$&#@WOyVD@X^-Z}kl!0Iup!VD$&J>r1u1%W^ zW620k$*0hI^H0)4BpKQ~>tllis~q1=RgoY3)hB5zCV+MXalCpfzxo*I1m4IRR(Sk@ z+cwn51wYD;I4heJ`nODob$gh%$P3Ypg;X%bfrgY$80FDwo!&}+`k(()jeoiRQ`QPu zx~QnbwM(I$-%i)rWcg`%P*D&wgctOwRlB~d%YRM0l}x0orHs3YB%*nnTcvAOFSZS4 zgD_jT-hDAEsJnz|6oQ$9+y7D%^D}L!alYizZUlPWB}{Lf12a_dzypCbI*4Kb#HvMy z5J3>z$mu8dI@*|qqzA>&14|I(FXKYB`Euz_jJ|%-o2Yia*+nGA-Tlpz_<$y%AdjOb zlvz!rH2yPRm+sQ^48>@qe@Dp!A)FX(FaSRI$Yb01af)?V%Pz{ov>|K|8l?MdPciH= zgOh5-8>grjgV8hrUFt`?@bSGVg|I?9m~&#dI1^T-K#6qiRL8>U$38+l9@ z(+(UXnQC6fThUT}lU|JD$nV^ryQqOF$^9HyO)6r+Y3DARAvBNoKTQz z^8+-5=}jqK*A zwBgC;tZTW1V|Ar&MSlABP|eg0c-NJsmfTt|swoUpOXxyE3Dl7t%a^#>?iOdC*>s20 zMNmgPNgAak9`S$?)IJkQS@6`>YzKJPm@3(>xZw4Yz8}seswOs1Hj6<+lrvpQ6+drA zB-xti7k`udNb|^^f46uQ;li&fuXaw5H@!Dnd~`fCHmS^6gnOHK`@>Jy%CG+%D(~O1 zav>17+dc0(+neaFN1|mqeikc)8iDUz)X|syKm7VLH8`$V?S<%{0=geb1V6XA!^P<= z9}Dlq8u|Cr?|%Gpv0BzjSlPx`@s~E#u`hM{=ZWIar;%5RS9YCM?nLF=#M z^d@7)CI~Mi`40B`srTnBYe6BD#c`b&NH5x{B~2cPh#XL`3sZ$n^71X@mQlaonW7NN z=AFCwycwqR%VKy<_F6<8657W}6Zpi`k%~j8OO?$rS|b3WAR7@vn+-F((s0k3pdC!7 zYRo{`a);_<7n8EBBjO*d(M+-P*twO(9k-t9Hp0g45--0#32l^Unll*+c9Tp@GS)?G znRxDy{o)j+<>_|-u!XK{Qzt#G6h_B&Y#>r7fYh`49>Y*L-S7~$>VDLlU%1uOt@#?y?3d;>spLjYJ_Fsl>`gnPT85Xaua=eg^L+;t=)1(5%*J|?Z|skV zL{a%Dm5(|$b55V<6Gj?_xc%@J;RgcQ(MQ*u-G7d5pepJi2NdYK*Z(;B`2TDn5Rz#G zCGJZ2em@^^FR^f&gF3NE82!{&2BTF6eOJU*PGy~0ta+H2rjojS;xQg0s}kO{S^(BO zElSy_4Qk>67!KOte)nr;ErPmx*jC%ndYcWgIRLR?9 z%%lqnQXS)R&9LlljndF-TbbnkTr47fRk7n47W2STrB(N(w$Er%UyjIxvF$|_&NvEY z@=ayzA85m&P8NqW{)B&Dq03(o8306VX&k#G|cRO*;m>lnmGJ3mW%Uy+Uj7mK)aIsqnA(0%2+-V ztTfqxVi$445#sjyR@?lz^WgP1``-S=vPwS|kt`5m-fP8F?U!M#nfE`I*L^6tLy;Ob zI4!P~Lmb~_T^N6yEbzrDt8MB|e>r;ik57zC)5&m(lzEf>l&O=+;WPNiGlpe}O`1h^ zH!cYN8PZ%{T3zAq2WAf996c3X70EdaxHlxDtNp*JSXXl)@!Z~u#^UNSEcGJ*?je3E z1?R1M%~5f0G3{9jU2`^Hl4E{`Kiw>@+K1vrg)CnWV?>!HXeD;L7A0f;LNX$kdhO0! zD-JMx`a?WD!Sqv7{}_cdu-I<9}p_)RWx z1hYpqLho!RzyX$Nk;|&r9z8?p|A*#>vmA7@EaD4piCp5(wz7|0caQ_OwyID+ zIAP!u@Ds&yADze?g5qgPV>Q~=fW&9$btJ%;Rb-5>%g5IEdca#&R6f_zm1REubEqkV z>bU;5qv^vv$O4SqO#e(r7_Ok>(_Vq#s4n^oLG%;= z_oPT(6#+SwpNn7q?k15_>o^z|;V?sUed~n4s*f$>%RQS}8}9#0H0ui@V{tgZdxG+} z6XiWDBOlwlut`kGDEOa4aU15@&$4}!8p=53W@qcjzUUTBktc4*s6NMb%OU# zXM=z_N^VY55XczE6k zRErF~-u~hHMh;}EwbjsP?ImeRYB0;IdeOGecKLACZP|lWrhm+fy^!kH?$rvv)hhhYQq& zJxnQOv|MfuC7E>$lPQMB3vQYExtD>RfL2O(Q)gS5WU1LG>C&gD+w{s906;*$zx6@+ z=~a0W{0r*G)qBjE^*2AH@>z*yeSh7Ok@i<4PpvtdWn|Nz|4ourwexX{{IwF5=S%X- zsIY4o{W8mm>(f0MPg>ni*hVQD7>5Ibq1<3Ajg)0rlMT@}&-IQp?Z=_Xl+;_B;%iYx zBMx*4JzR7yoxNeQ#SsIGJ{xLVO4n1!2IR!csH0HEHDm8bPu?!5vP}6`{?HA)hb`>TT5j%aU-_E5z z+g&;r&u;v{I!s9<_tvU@eguPDVComWJ9xDb{tK7_QX>>=-;hIS4@Cd~KmbWZK~zWJ ziQ9g2mhZ+3nZEe-)n@{fp#_qx^?vyKV|Dp`pT2x0DCYR4fAL=1;v0ARa)0fbNfzc; z!|Bn5*cq{{EZKdWhQz`wByC`M%4In}2%wuP5jFo0^D(QungcU-bD3 zBzM$|i_V+9@*laiPR%@>PKGbwUYJYNfKo6d<>WTBF#*@D5kSr8&C}*-OK5mrjF_j{ zS=N8C%}-*WeHWx}-aLueq&4C{#-6Tk;|sW=(p2bgJk(;2`z^EkSC&Es&(VjCp8Df- ztySUzmyJd70J=&F_QFK&A?dv@a&%c&@*wlSnID^)j`E_m<`U8hdNLdaxnTM3dH+SWfHH^v0+ey%X9WMv{>oL6XtK9arp;` zz$_9w<=^12Hee!&<^HdAwB3Wkv~C_xnz7+{-LQzT(VI?BOnb{YIz4!^g1@-W`;)ds z=j+*tqAZ^FSX+Cha+BogxmSaX>M7K2R{u-}EZAR`z-+*Vj-!Hs`QSYrzrw@;JtlUq z-nSpmj0-=KC+}}%DSn$iYwuQOeUz^2`z^8LlfXLu`N<@gz1}eSZ*%g~3d(X!R8ZdO zTP3KQFWYlPxEC+GE#t<)BvVha}7n;i2-}ZC}l=-BY_m zg&wfI*OKOV(M$Tw`SVZi$bbL!hrPTE+$l++l3LhD;zjB82m+a1;BspQ`=z zCL?|3wpDd}pYEn;MwCW?B;`>Zp7}~ zL0Erx?oA{UImw2?G=%5sF%2xOKX4`R+&_J}4iJagW`5w1nZT>o`(y1yi8;AFP-U$X z#d^tejJ7Nfh@kJv?3?n~PwpIVtBlmq!Ryf#|_hZ zrfG0kGeh)u=V?exinQxUIq2e{on1HP;c9{#RY@0AzJB(_@pt;!QiPU>Eg^S$=A6!A zd?Wamq^&gIj$vn9q?0EL6wuvBQitbPvO`mJ2$HANkNdacnE(GF;?xGGb9Mx;63RSk zZDeYj(w~5PUapc?U00{U>lc7Fjt+bl6FH&|T0z8Gw98(;rla5Qjq$C8YUm3>vHL@T z0pt=MB2-f5XS8KU@|i$!reV&Fa9d}!De=Pl#4th=6D*Q5(E=V*A8V{(!xv%`IVm%R z<^e0Rf~*7!x=S~^7++b$-&mFpimQRk4WS4t*^q<%#C0vdsarO5J|XloA~mi6BK#|{ z9#Ib=Ey)&OkRb;c>o?umkz?p81(;h)j(%AIHDAs~I<`UJ%U!Do4 zu$fa}upXIK(gEWHJY2URB^m0ExE`{JU~J>|M59BttCZs#EwE<#!-uSKO5A^{s+=kd zm0|2BjIcx}B714@^BZ@cTlCv1Z&fgSV7iEi(~c;eEt)vgH!_pVs(q1MluskSnFP`= z%n~R@G3(|9_+0FcadO7WCUvPy@@asIrP%0gl57<;ue?#|hs z*;tX2a#3@*RjepG9(~>daFl~jjn4!YK6y*6mkh0B!+aqk%`MRjp!4D!o?2$3t|CIb z)9-+iKZ33H%6W(4pb2^Aq?0F=703mtwQH~n1JC> z`sNoT_H^E_?=7i34xe~Yy7(IKyPcz1Zmm3FE4-CJB$#%6UVdwd&h_1-`B159GMAM( zcSOWlDCK426t!M?q!&`p0!>*5N_#3T54?sro2#!R2OfEjVgLHAt{m?VlxSEKp;tYd z^j0h-sAB=@1F@@i*_5R+f9DoFxjr9~u@y>ZMfoA*(+Uu49YU51uG|f$HmiyY7By{2 z*P<3Fys4wHB~i!B2bqhcvh-8fDz20p(DNj~1AM^FiY?Y#Y|mB({c`(kw_X zSdv$E(KJ(Lw1adeSp}FItv(Z2IYSU&lHxt_)C80c?^gs#QjXnYkpG5uPo*m+twU9Qdo#@e z0UN6=tF<$^db6U#&Ys!pNcG8KM6MC=Kq>^UedgUx-@E}MahkHdZ1mM_F2JJ1Qe;6d zC<^I-p?>80tRYj;t$lw|N+=q|jOHQ4CJFy--9)=hfsZcof2WwISbYU9-Z?E@cGx8t zA}u(=F9?drjg`Y}*@*QPh@gRN@R8FzDd3=61nDf7ys{RZ4{ zuQ+ei`D#POZG?1K`)d6FG)Uuyy$r$=+vAB~nJJUJp-$;)ZVsi03YK$%(jk^W02^*2 z>x+m^R6J<`nKU+QxgI!h#$ZGN;AF_ohJZx)U^e*;WQ7`OAm2S986NiPcq-)*)7fBo z_!M$tXBP^$QmI1rfr`FXKml?;OV!GmSlV*D+8J+@<8yNA9DMCyv`oct*?d|Ak;Giwx$x24gVANJFsuQ8?`T9!~U z=cf_B`m&ti+>&m)4aLLq)8L?+(;S5a2z_3L=42v`7;;B6%1~fVMi*7!AjGT47N=RH z<2=S35;y+Wp0f>`152*L%WO}t`I6975q6r!kIf+==ELh_;tN3#6WcaopX zagb6g1Ndrd0f=}`)&M&1%MaVODBbmnQ*srPS84ZW2&A(4>ReCQkHg(l`N+&=(HIAI zmLAN?6XMJ;ay~|4dA=3L*gJ+04j86t)rtvvPBz#fk)XAfr4OU!ZzbkH4i&S{+Ju^? z32aEBjLyG@7%Aghpo9g#OB{$fuoi7GAL53GCOs)x&s!cKG!IGjIavjslKF*#0}&nf z2hg0db`K|9eKz}Iid@}{S`h_rtGJ3;s)xUPWg4d!+UMsda$<9MzpePRUD=FP05NyhwOpr-gP8=7)jHgfysct^ zwcIXw>+hR8Ynw;TcW#@M?@kZb5{ykj;0wYfnAIn9z0{Ya9N1+uqHZkd5)ouSi+* z2_n%$+S{v(U{om{CH2B-Z(bOFWQC%jc$-qA<{ZSAlQs!zfH zK2v93n4Y$vGQ!?F{`|?-vM%(Pbee66bZc%h(mEg56IaXn{tO#+^%g zkjHT0@Urm$_1@m9^@FsJFChTi!?+i4;DMC8ViA?mKTIUVci?7*zS&2&hi=EnaXi)l zDStVsKh~K>O18Q`ZNj|wmr3FE%A0X@ED;{7CwiB!lPlJnD|#g5i{krBnRE;iMC81C zz^|G+(zplgwz8KmVtEDd6GZqgpQ6&$CHbASyR!G0BwZQC^Fj5Q+_W>VcmTkN)dOEW z>WEhvQ1Atlm6`MO*8EH2xTL7-qgDe+awp^fa)Ea)7>;v?nklB_UD*1a^eYP5x_2AR z;ITk8n>T+4%Eiv?lEgee_bGOE&ghbCw=^C`FZJP&GUU|Ep2MM1sus(7lbRGL6os0b zA58Y2*kaY#M+#}SEWk3#F7t-5*-%QGGjsx_HctHUm8^XM@^a^LZ*=C+^Tb9b+5`7t z7`xmx+n^FZcItU`eb_k7&*ZwRvy+8!XQq}#s(&EkIzel>X&MiAN!5rjMKsD4eP3e ze@i`8?wVt5jK6rx@9+0LS4`=b!@HYqNWA(LZ6FaDO%4nKL9cz-(bl`#z>$T#az5zH zV(!#uH<}LR=qL*35n=m+#R-TDmy$<86I1-F?brYzbM`Y_dZCl7ReIQrS|F9wRiFPR zB$iwBa`*-CG;9n&%BY{3@wYU} z?kQg6sJ2#bU7RbP8-EDH>9u8_68V49+9T!*K2T)UWJ(?pdUI{2&%)x}{x~Ak- z`a_tQ;&S1w6co*kb8SkgaH#qbbFf$iz(baZP;?nJXOriG$eLW7pvs(Ltxr(?nJx*2 z>h#;^3{|jtuJ8`A8XQp!$%z+oqVKChKG2?5e(ajNxiprPUk?EPn(;W^Mj6W*L0pbV z#)`H+yqz-u6X**=J}08rvXenF*s>%MJ&VCJ6g`WqI9&OQ=Aj`95|hU2yLP&mj*`b$ zno^4556D|(Skx`?25p1(3q~uavS!*#R6i)$lS*OBN%Vr)cOvV^;)zglTmWJ#yRte> zSkMWwSJUp14l6RzdEV-T@92jh!OShSlU+vOcku{mb#HAr5*|MHv$5MfZbZejPQj_* zb*QE9Hm9bg+-ql+_2&pdlprV9{GQyEUS;9NpH2=~okb+~eEfP%yqlJrGn**NvN#ij z32|K4vrMVI_z@WoFjg27HQ;Z1FLI?&)A3?US){+kr_MjhVrV~$6Zvi|JOh)=S?`U^ zS}9H^Dkf^~$?PnFOZkJykfegXP{u-dS1{k#M><{imcu}#3x>C+VXD>}P67mI3vL0@ zkT{7_*I?4f)XsTU-Htgi9K1w3z0OY?3|9Iv7q6Z&;Y9u!{Q{bJHf~-$)DflGwa^{C z#Wp*u)+_N(!Mm%r0|4&8K4HT!EnitoOZE+8wk@KA5oo&Y387*=e7X*fS<1D7-dLrN zo&{MBr;Yx1ec42T5}#D%FuoAHSIvH#weXHbpRRb*4-1gK$lau%!!M{89KpN9?^Kph zblVW*sN}pN(#&3FOkl}~Knf6fbmCz&r<^XOh;zvjy7C-lFAiIiiM9;9<1}P9E}eWh zL;zHFeB8}n;8iAj+TU24AE>Dkfitv>VG9Q=IH%-0O>vcPu^FjR*{<~NJX?6etEH>f zAFO{qLOIterI2q_og1uwd?ott@T+zn(NSWQX&;{^c*QoImUqL z@LD@tUXxrxD*QpJn2>6y5A@Wg!lHVjJYQ-YwX5J_$>lSUQKjLk$*gWve9Qp<56{hA zcu*#)9pCX&cAa!kS!Nw~MClo^W(2Vu>3X+dly#9dklD;ZE_uW2I~NUaQ-A5ab7_2= z=S%g2boNw;boB)$VKXo6&zVQg$O#fLdCIl?z3CtJskK{r1yM3Ns9;Oo9j*K&w1tU2 zxtMn3b3ImuwGfR}>@zV-m6nSBhAaB7toq8|f4`LshdBY`*` zPU^7jg+=j>dAzyzfXz9-7qrdGAwBownYFPR4v?lg!562-MEaZr*jE!+Y(81$4}3gn zizvu+sVEoSo;Io4*u!d;AHW(&H#dO>np7w0%_vn@YpeNzt zprf!ikzdh;VPJkG{cV`NG;8c(|)b0g)n!ucT_6afd>r9?G3MnOFJt zThBYPNMsdNjkT|Gvy08*axJCarnjflEVGG<=-6W4W6b511ar=B^YL`WFr2zsDe%aZ z6AP4^`cbKH97F5I#|Tb{VR?k`-L2iyH5Q5>2J7;Pq{0cBB0Uk*?^@)e6X{dH*EqZ# zipHen=lip7585+}6p!8L%i8^DakM^#a&Z!*tJ8^0Z{lm6y8Tyez3)9jq?T4Y_OfqZ zqjq+v`_c`qLFIf3QhF^wL?Nb;o4Sg$_r@;q?b z{I#V!rDT9THPfIhv|$5V59B<0tfHK99I9#s4_s5gSk3D1#mc7wOSu0zdr`f-1~P-5 zsKabr5pYlV50d!icBxwUf(5Rv$zfO6RDK>YrU0c-z_r)Cc$T?xU4o7anclL1>Jsl4jCsTa1CY{s|$i!0Mo@%A7SH|=|4`8 z4XRB1b2Ul`lXL9t(ve?@@U(E=0H*QC`WVx;SYm)B#e!_*Q(@l*9YWbd%pNzEWh4P( za|dE!=f3TiV*an3-@K?otSd(IYjwHV_l5HU$iw=2Lw|RlS>zTdGhTQTQ7d)5sgTHM z^z77OSeBR1#LH6Hj86Ng!I)<5^7i`tD#25Wb(5>0&^Wh71zK>{>-1;p@DMCcl9{G2 zS>85up85YILI@Y9qc{Kbky%LXrze5N0H|<`LKOdqff>!iFjpClwfz;=wi77R(FT)m zYq32(^IK=)Dv6mzhU>BqXXEtM;kRhV!m-&SKhZI&s0ANP9Zr<2rMVYq<4`(!Ta%I4 zeOFeihn;5GXjKCX<6{o}1os5&U`bYR(Iu39k_RFq6Y(h_!0m zzIpwNJYS|9L@S?h5Wq$}NTpv_q`_9@d1$o3m?Rlnl3ZE*X#Yw}xmFJ{XAm?Hrl+cjOaKU+{U9$0e#1o$Fsjc;Vi_Ne$_xG?e)%ROKx|hk~i&M8eRSLg+ro2 zsywqQoqb5;1l5+k1kS;kOZLlEoFx0;Q|D#-N-~8)BdBk_h%#ZEH&3C{P6ywzRsbD7 zQz@JwFG$X56-v~2daA$t7@gWR?f)kNSlDneRaa-&d9K?jBVgBo_gMiWt^UzyHKfbB zXc}BYT&xEq+J#IxR^spNoe=F6Yj{SRBiDUKg+nJwv8m7?$6++-+1y~QRYv_YDoW3I z={Z7P;3!@_zdr18gLZC4si7RL`XrICU%vYGx5GbR3zghO%c4Wf=$hQ^jVWrt2m-el zAUJKcn^h0bzUU`EepAE$PRj(?5Ru~H)FJDV-Mc%`VCZ2&J?~68^`tmSG=h_z^q7vn z$v0@q{bQ6?6bv=f%V1OlcfsDfnmkdcUr+*q`)UV;NC18D`wh;U-Y6`6f9u&S_Mm0S z^0sYZhjBs!40?%>QGUV}9=Y_P)5wg<_Y8_xrwJFM6hajGD zr}S%*%RvgeTmvI-s&R_4s5j>%)ldch{qBXgHDP_&J6!{t=xQl~-#c+vL3H1Gwh|yA zQ-ySlpbd{LSMVC| z2Ir{hZZP3*4c%Lv$u8$4iYfA`G3@?$>g@IXa!RUJ9i3S(~4Xm?h+q~^q$aKq6wOb5nmnocQoTo zPUzTqQe#Lp>*``8sL2^<<5{qDR*LqlDipbUvTeXCF022xSq7RKTZn~JR*3`{j9nb? z7HggOr+lQG+28dA`Gb_kaq6>p;Cy34_md>&iRq)>(Z36$<`!^!OvCY%EJIQ?c{W|1 zGyW7ri~!&+WbcXbSc3AI&Gg$>`C^>M&OL2<^;H?I2sgtnPUZP^BbEJ007B!)$CzRs z%VQEKlwLNG%68k7AC6Euhf}J3YaW{X_OI%HIe3hCU@EG-9ii!C1jT1o#F0w%PGiF~ zx=+U4uI$x6>d*hihnD)wwt>$dBPu(mMJiq2S`lYLLEh%zZkEa`l@Y1m@~Wx)dlX{L z>=9TR(aU*`aS6Q)X+cBq@*{C|KpO&syg{4EP2s+I_;s5yT~#`_cK|j)cH_fH=>u~0 z{*uUM7e~Y)Dhvxd29ZS)BWf=wLHEt?{`_j3osktA)lt%zA!;v%IgGe*l&Fpo;t@U} z3JgW7zew>k?=e=q-1fs%8clTrd#1$FRVG+17lEV{C_~t1beiVDsip-i!BF9Md6$93 zzQ@T*zpb}wXRl3Yc`?f)?x4)efoaGW;2zl6`{ZFZE(5j+7lm%v=^s6JF0tdm391o< zk;pkPDAzEWLp+d9Yj}&`Ph=|+ufX1Fx)g-;yirZ8QznB3Kqm2Zfl7lI;`}pLMXWDE zD49>KeYHsPc@`;wt?o*afCB3oXUr6OaN)!C@>ZLBOdl`Sa*48XHHSjSkc*hy@>}gQ z(hsfboKNcfmgm_pJqobPTL4NaRoS*$gbjV2f(7iPd_u^^no0A}0^UBsZ-;CrEAPIA z3BpsO+%tWlPI1h`aqu#Uler&OL>z@hV;h`dnB8H(GnUM1Kw_#}LZ0?c3&brT<4hS&Bi) zN8?gnbuB!BJv2T_C~=e$+~?BG--05|%Gv5z3Z=!U~_^;pjQ#-VCnk zsJvZo0#8g-=q&B0ZR%W8|4C)qDY%n(cauBPl%2A|wHe!svs-S#ph_dU{BPTlNm;SXJVV2``qM zYcP4W0|APM+K4%BGqDcs2=ZiT7FIYO9X`ccbjD1-xwL6 zp18P7xJQ{bCr5If)T`yTOrhJJC_U5L>PPK?hz9EIEDS6H@FG#rh+j!JOCB#ojfEZ3y-^xEIKuI)y+I%qHkxNS6!R+b!0g- zXKgwC?#HJWj)NW>FUI~f45Vajf8pPz8VZ$9k(|n~Iwx=M9L7`m>oafCLUc=eQE=}U z8{+&d@Wjy#)Sws@$JzY0C+DdR>f}HL9hDhT(G)XyPkRmR7i1QqV6ffI~3-eN?c(|U%C61eZcf{vIXG@nxjv+xtlM|Z0uPq{(QaQ4s-@bFvkrFYmY|s& zZa{Fxg<#d0H8^fC`-rR=WC`XuAwP9*rUjNiX)jDJHAS46VmQ7J-5cpsFR&Ri^raCp zMl!r7h6EU8n>v`w`7m?my^a)Wu7t4!^(W@s)-iQ@dl`(9t2WAdZ|Yo`Wkx9w2aL){-dw+< zOpDN@Q)Vhe6yyuKd@f{39P5#|CD$gI&$yJD+*su&lMs=n?O&%l3tWFZ^no&ClGFn*8V%P`|a)cZ4W#~ueN+ZR^`ENtGg>a#OQ}1w0M8? z4o#mAEtAGXU;j(?b#r3gpSS+vNU$xlMw@*c0Msg;c~Lk1TN(vPW_J*XdfmBH zil)6x_i)BIsH4ogS^oZ%!DF(WNk{hb>h=@A62ixa(tewA*Jy;p<%3-dujgiD%=XkzF(}FAx}Yh;Xjlhy72TR90wo8X6EK``7b@)2O^s)H|IJ#k^koM zx%^7l{NrIQ`9Yq^vNS)jXc4 zcMSHV!DZkm?K7Mja0b*)RwL^B&qv{!*87-Yyv(NK-UEocBz2~{3<7rzfO2DPEX?;5 zu4v^1in#xkQhuj89)1u_=drIoYNpskC+16dtApYpw-Ju8McoXz4^$ z-ulrWon6sqOU>_b23?}pXv$BKh>zGfaf)MICr_r+7Y>-KezgXxc%qNnZR>kSJrN2x zW2rKjV&{Wln#9}jE%FK|?`_lWU-jwAYS4kBnH~})Sg+M~BW3o@S3pmj>Bskq!kqb< zn=QEeOCG?8$y$zAMbU9I8G4#(c|RNkz+dH+aH696hD+Rz6xLAlF-vvzCg0RdGJ#&t zqQlpht1HsO5%$`}v~yp!M%3MplP~@(>d1n}Je4YzjF7NUn6cNmDc|SQk%Sp}beEq7 z+b|vTQk3R^psoPN7zSZ>CaGwlI9ep34)=_sP8FF5 zDW^o4z(=KEiusO0cp*EgeK0Wtc;DKifT%JvjlJ=^$qU(-HQ1Ntgj^hgi7YffhrAFB zYs>KEG$`MEJ1ZswjpL1wcP|3~HeAR=^*F~F)BI(QVWEbADn6f8QB z$kdi^RtInLI$ihWQ?}RC*`+v@E3`Ua1rl@WFwLZVw(5@B3q&EA@96~FUz#%W{o4Ea zWb0j3NP$RyuGe!1-;ZU*y5l&TOEFp?hmPlcyYr7FrpB_%NQ265Ld_=7+3vOQ=A&+0K_ zi~tednEy(G8%?x&s=8xT%H#PD-seg1lGiG46(Kez#QE7n+6q^NvNZ}w5pKGCZCoEk z9A&ryxz9yYb-r6Y5^WbJgFD1Gp1jwu$0zIYiO&$Rsr{Knpf1NGlA)RFaP1) z+1LB|Z^^7j-LqQv^LA7=l}{)v1`1AV`Q@`xZ1&=m;PHf?fe z{?ZbRD4WdS^pJ%m%aa+O48Iv`P4`M|2U0i=s@@lRPBEkQczNguqsdh5KK^tfav`+G zoL#e8&HtnGn=A#7<=NQ)^twdSPxjRWDo@71ok4|g#W?$9Mi=*-XFJ9r$#UL^+{3yN zV^iHV+eJOFFrsSx6<`W@tX0(t2y?#Jh!Ku4qG(LpQ5dSOW^**zgJ@)TSNL8vSD zD*JPwGC|>c!k(E3)z*{WMO$$$G>1zFlH5)z?qm>-v|7{CL)DO^>2$zi*i;Mmk@xQ zN2lL8Z^X#^(5N|0v>*CB_xrxDcyP+X%}f>^fAo!SerBuSgg4E9p^o5VRmW39Nkbs* zJ{eVW{S^^k%9WToRNr5XP9HKwYdkGl*Ck`>E#|GBHdbDZzQM{dPOTA`H?TRCsTr!h zxl^G~Oh}=r-2IuXT#9hgC5xO5nkO?-;qy-BqwA}Mb+v(0Zx-GxDZ|HIG+3MQ1h(!J zXZ1-;P^_>o1tF=K3ELsy26Rn{i1~{3+qq-D4+rvDUdgwae6{}hFP`k85LvyRDl+Fn zswE>P5t!vt^vo*idSrHloM+){wmfX| ziQ$pO=M!KOfM){8hq~}hC;L=jO;@k04?Ww9#|4z5S3-IRWfvNuyS@-lweCiXMEl)$ z!|Cj(3ufX8oq`1!UcI1^RFzxTNR*Kc!?sA$#Z*6ztJlNMoID2$say8piEe#&qR_^E z=mt#EJ7{g@w+8~v;Pj+8+`f|2H4o>5Gb89;Zaa)2L&J4{R{|gJR24gwr$2=Mj`R%* z``B+UK?dFS!8E@_jq~>9@-}VIjq>Uu+XniaocW^N8ONOp0CtXzHT{#!%lODyutYj^ea6hITBZ+)2e&yqfU_wB13>=ZHzPu{&gWLj0{Nvv&TjiJ z*bp@BN@25T=V^S>dlc34|6o2RqQQ2SDeVv9b7#QX*zn4WAZ!>FYxidEE2m@M@$Z`L zDg;HPa$j;AGhWMEIBBKhjKt$HC2tfbrfEo=o8-_Wp10!GsvfdAHYZ zG8J`b$b`-g4Nsngv;op zefiWtdugZ0R5E>x7ztMI*dO!w@>3v)EHW(>ep746joNmDxX3kgi{o%S-4Q9K<#uum zy3ec*`J%UQf3V+niPrmbJF!92#Law`cDLim{@f{*lMIUys8LOsrs8_L4Ov&~+=1+B zOp18$?G%yQL+JFfoz#zJR8RPYgF#^@V6pn(B&%>8o8u#ZL){W?>FF;;#TcIQ7 z%pqNly9gULIbMoa`hnsi4pOQcj6A37O@awW7%z^y0lh~A$O^lJfBCy3{V$Qn@}sPGCT$G1~1p&a^VCg>PcxEY^qVZSTzOsJs{hz*Szb6-5^74e3adNSqy?2(nLQze`>spNJb_qF^$S-GIx7is z7~0qt*#;UAQMJ&_cNHK*$}N5rKe2C8#&s#Y9$-O^3kv{w;~O#mEqznep2)GV%}Il6 zBwPgv=mb$R2NjW}JRIjt@ax{;9PQY*pzC}Jv@O|97W~jut%!+>9Cf5I#jz2)KWQy=hGmVn0ts(nB~xzUh0vP|G%M@aDU9o1%T7@>qVwb4u;s}|!ZEpG#wv}= zRTmrk+b+-!#|Q0ebhz*u4!i$gu3b-4J3^)^#u zJ$pdL2-u^8_`#H-GZAVT#wysJLupQn$_@$1<_-&p9@8p3*C|ZxWl8H&fQsMdzz@&^ zm%(v+oR7Lw^P?4feR@&G?^wK9#>T}xZlnibS3>#lqFC-iCVwF>a~gT-jmlnsb5)H> zuOC%$J!<`=*VF7XrD-$MI1TczJnD-ri}}_i+FoAhTf_tbWAEl-+y=}Ji}yoPmNo}p zCJjUd2}5%t!hJV7n@7!Z_>fRBv?UBHD#35w57KmEu~cl3qg|T`l3Aw9;$QC`tn2ywy{wEArA_|u^ zrC8{07nF-8hC}H9>pW7{YC!@Jzn3(^C+Wm1AW3+h_0n3=*vmIL>|FK7?tPc&aFHuF z>4n3r{`nx>KGF&86`uMtgDyI+Mwxouz|j+ypm0ZMAgl;z(B&y%aBC?%5J!yc^wf^J zcD?h6b9XRn(tDyJH4g^-RUeC@$jnQ-v84fK(z>vu_*h9deQ3?#gn2Zj;9iLqAVh3v z3oaW#zWTQD807{B2uviB89P?yfUfl@-aV-5l>X58T25lJu0$Mo0_KU~icI_e(ykd` zXpSUSiNUA%KPG4u4cH;I2iZv9&)m`H&!20umDYCr2d4U?do$F@4HGsZxZ^av#eUDe ztd~oZ3TQvFN5-AoJxQ!UIo$V8ahW9=|3NM8NFZ;XX}5W#eVW`Cu?=Cav6LvknJ6`5 z?McRjl?7Fwrh-|9Bh7y9cFzJ^G6g_}PqifV&l2Ir zO(b6WcD)Y(P$i&;DYFqzx_eEUD-57g+$UGw<1=p3{y=n|7I$)$eT=$Yw}6Ll)+e?WoRO|Ud$)vsNmy*B^0{jTK(G!vqdAIPvL zb>-WH!$s8y^OZ=Dhd4a+0jM;rl}pL=9vk_6+ z-R`C9Co9X^fyL_7$+zdUffop2yD}!zyWKG*1`2$V=xzhyW{I%z!Ue%!A{L+@n^7KV z&4d}2P$ZO}s}VTW+&tM%TSRlBg5W6$fr+UgUr>ekU@T_fMZut5*38CLLR$?xRxR1w z4E|rxiZpsuqs<2!mG`B|-WFGQIz|pG+&8zOvk8_1JYL)6B#k~R`tmYj(~~q&X5Bfz z{#%@35*sq6rOYl_Bmo6vYc`d3p{isn%z@n9u^|JTqeT2b2T4ksFqN- zz;j%~1Uw_}}N| z&fK%fC|!sWArC*_4`{fu0suhP)Z%#d4Pc%$v6_?>}?9liAaAO*VtkrkXMVoYwWE-bMZV zPNn75#K`WcdbxDp&~C66Q;{ab#EtRsQamS~HM@S>16Au`l7K$<6XcX9zg1TOIUmKi z6vKP!nYU(tI`{W>KZZN;N_W9RbBp*umiaWRmYm^W;8^JA6pw=pVWU0p4x#mhlopss z)!@ot}w=gKtk8o zJ+;nm=x~5tx|Q}glE~RjgEEpDaZ&bXlo4niXQZg4RY9;VdpJ}!eL2DT{;TbWZhFW32IGPb98&MoT5LyVPOLF0Zr2W9 zLu$%>$PxgL+3qN-x9+zd@^Bl~JSu}Op*yr%UL zjaw1aOpz{n-t;9662dTu z53Td|?LP!07Rlc5j6OnWn-)}MZPga$X%TU(KXNtgj}$hfteo&f(HA*%6XO`JfZ{y6;va<{c# z@9KB=;g?^`v1QY}W>-h9;}A>=@ul4acH^$Ewf@u|tr{RvFk<40PG5Z7%hVzo+5<+G4W>i3e(W~VO~C%npUF5~1^Z?GmeOQ@k= zo;`4Gm`ZBwgdwL%E#`=8q&)wM{EX$2j#-t_?JtoI$jPTfwv{`8yO$l~VWgs!x>zb$ZZ^?t@6^8XRW|t+faGaU047_@F;N&tBU@mbEZ!?S!qXZreKYZ zG66`){Th+vx{rqiV`N61{T7P-nf@GxW&@5u>tRh2yQf)XP(V3GYLnZaThVV_C#)bl zoEKvzD7wYr6=UaP|GI!9&L(|GN4m}^gexQhpd(zgeZYD4`QR2i-T3{lB}2(cYPruu zl0;(6xlJHfzvoOPU?DqlzhmKc7{p0PMY{C!B9gAZ(z`pJw$_?9I2UtUJLdv{@$MJl$Ay)t- z(cic2ndOqSBdY_oc8Z0f?LxV4+-wliwqinUvhN`0#SNQCCbkAmm<>dO>(?zmTA;E) zJqPM>iE;X#lA8A~bD*eXqB<9s=+%gA@JT&+FuA%2?JCm`07IKe&|wMDb}Tr%fy@kC ziQSv!=VQJ!bv3%EA^=7}xxe-kKwU|O6o!7-CmsE~V>PEp$aP{Tx`XTl6g%+bsp+`dUKA(w?@cd}rlXtb%=GeAg zmbkQh7xHOZC7V+@s-;nLBaejaV{#zyri-~1QMi!=J%3DSZ}YTc9Z&bea8ah$97=S@ znf89fluA_QLzU`NMeL&wK|(ym7wLx)ey&2XcX*D(u)T#V>SvIl4%fPFuh!>DE^5>L z@f4*W9Cs?W+Zc!d06+jqL_t(C1%>x4&Q}!JyX((@0dR1rQ$4$0FT(ii_nmR+!8Yp1 z6wb6t=dSo*8O*Y0snV$7n8P%Gkpu72FrdbpT7!0iVT%Mcl5Elt?w5N!^ZWSF z8Q?_>d9KK_rmfcc$q#vNh#gjM*Pv_RE~|s)%~PIg0pLp>OCuXzuWr?TKb1V*WY$i6 z^OtIEr2NB4I$cw3gpptb{P1eZ3Y>Ra6ne#Qkok6Vd}@{G`iK~&NvN!FTCLg@0jj4} z^>H|;E}DW^-bKd61tRSSx<@(N?ILsf)30cXfUL`9syUT#{WQ=%)1XOj&6tHFV+r7g zjPr)%_*&`#Fy&VhtKHW_-k4mQDeQL?r;jLHAdjMs(I|jGx>>#q_Ts%;FWtB)eg$3< z;t!dMp-5p2*fbyRii2`Fc!Rd>u310oc4{-bYpkq=3|$aPHjNuB@O-N}SziSoch+VZ z1ph!hgC0RKXdxgn|M@IStgm=SPYlif!t5F4Gvx#TU72(g-zM$$g|!sY?&W$Y3hM^t zwm4Mf8C2{z2dZ&a(gwW0G_1G1$B9h$#5N>&va)6;;%f@V$BbhS|$FMj0jtodC%R}N{!Je0| zFPE7c@ep+kGpdOBG?blSydNh6A*MOjT;Z@2joDiIBFug4CmdX|0{HA_XIq=&G)$S? zNrT%TWd=;Nojr|*Ce^L9Ot}#Mxcv9UH#3E*fLWf@c7LUHPvk}U`H=<~alvQ&o#j(b z>4cEhuW7~gM_Ms%IGhkN|~}>-Z)jV zB|vm_c`OIKXsPTVCvh)$33#3b14Y?1l#=!@NIu2d3|y5=m^7Bm#7s)4*nrFkI#fC7 z_(#Kx&W}nl*rzmU09YaLEZ_Y2lU?N*kDnE$ERQ-i7c)9{W5TI6?DuQ={V_U$^EuK@vURkhD-%?;pp|DAPCd;ZH*PG-f>B^&a{%#hEmx z-o{U*24~h#L$l7(G#`C8j*rsnRSeo$vsEGJb?(TN&X@Is&emM0`>%^Vs3LA*IW!BZ zj+IK;lmb&8%Pb3R=Vbav@?0O?8|3V;B-?118oyGW8KMf*hNQC7vfd`$<2eCmBopju z60$cQ$}*_X_#hVVz$NO{{#HdA)tU5?ER}vYLEhw`4MAUH&dJ|gf49ZQ1bp<#x1pUbN^|1^jXdoHWTg67+~#wWpw25Is$ zTI)Ky3qDBI#>sJEQhs9A604djMiYX~86riB4;(=`)T@i;Ba5%g_D~_%kvW0hd~ULf zpy4MzTFt86fJEg?Yr0Azd?uOOi1}ewmR`yhoM%3pW2LU@H+a7~P^0W%G$yh4f{t2IP&9dfNI+BF@BtIIXeP53MV0R!8}PtAj@| zsZ=H(%2jZ+vlL(d-ygqM6~eV#+r`U2m!t>l13fP1X{qJTWh8=s0`Km@!c0NJ>oPwpH# zLJ*H;{{YY-isrW1zJ^gfgf~>2_<*O1ZgbSn0kYgnm+98m{;?h4lweHeJ@P5SeFZB4 z9wG3KqNnP_A*}|qdSwFDHe+;$G+gbB83O ziUyF`4SG4X*zn(t21%Occ*4L3wP(HG&=@ z-2lsGw+aWMs7J~iyiCrOz8x*NeKTRA-1ufR!aViAO)mD5L2CH-PQHMSuq#Us_Q9LJ z%$RJ5Nn-xoxT?rvM1v^G$CdLvi{r)SwAxIK_hMQ;c3;o(^7Y7BS5om?(H9mN0Y(vV zlboAAFj@Hq);O^8?yOEnV5?gjRCcZgKUu(TYYxp?#zK-Qv=hjmIu%974e&n(q%`e_ zzXn%j^XsLE{(QA)hA{~uRurB4hwcmMPy?_Q^6?O8|EwO5l!r!0R^x6z3m%G>`IquE zwzkcDB8R+ChoTMV|qg-Qwr)_45p>c&i`EddQ5(l;I_0l<# z;O!o1KiZo!vPCMMl(AUFnG72b)i|#Ss3P#?(nt9-vn%bTYC!P-tzixEUT#N6p?Myg z)bTjz!O);al=s9nd*+vIw6wMnpv1xO9Bq}5h-8V;@F)-n`RpCRG(@lB699>w8kpg= zGYA0%X*ddE42fDdl~wkW>D2h9KYygD;gM`F&UWHjH01!7WzR~ppWgm_7AwZZszjc3 z5Zvh)_;_^MelzGXkzbxP;)B-Cxgz*C43a;!`CJ88!77C58QDXePw+eBg5?jprO1St z$LysqE`ylNJq7x62lj+PRu6BCU0+t2a0L|0VjEaGx_7|<=0V0= zP^j3aisKiBS>{;!^xk&xVbNX^v!gs8*N4ddvPZ?PJQG29Fm4nw+n-2HxGU#T7i=`b z@PJM6q{o_keLH>uX9eJ?pN{A!Z2G%Lip7cbyFaOu;`r^Hd}Q)VHl_#0%xt5Ecko-y z_2VH$<6Lv^(EtN1bJjE=H62kn$ZKUZdk8xzcXAYB_9973;qvuiXQnk6vWEoHsK@!% zH01Q6hm;3X8Pi^|NlI6}dzgsK@k-^a(#j0R#p1EnC8GS7kG+YZjM(#35W|jZ2_b5b zR}4N{3Cd_kr!O)UNJGG4_<6Pvsv;j@vT`#56Hj}P>ha8HW6xXw#=cyIc7$%26}Rm^ z1V!5Vgi&D(LFWMi2N{#Nz2`gqCBP)i2xX(?B~~X@`gd>TbQH+yQ)ymXr$+K6cD;qB zjHxZ`QZXW9f=op1G~faNl0*S*S(hvqrUu6#IfxJeO3cGN1_=ppHj`A=$A8z+6e4z+Q9v*+KZGEBjae0;s!8{UeH6C-ZIp&j z|LX>(Ki7z^`xMKz*ZxM@WgI+hlhc*f9ylR1`{(z0k}FF$BN#Z=`&!YyG@16vTu6rT zS2sX8+h^@+q6iN^^YM_h10``CZ!T?|$ybYZ6ANXzW8hI_1s5Cze4Sb}UkcQN_48)4 z+c!O(1Q5aGFAYsQRJ1hGtPSw>zU(3E3x*oO2M6#>l8e5WHiqd7!jHq=D=jo&l$v#Y zuMPC3`CtLK5$Q>Y==PXP)+F4DazK)%3UPEyk~U_b-tf#dc$OPEvyKB#^%>7(?$m?F z((vU4=7rnGIbc05d-jdX(z!h!dAv2`aN8-LOolDg(>NyKq)}wOY5vwZK3(&U5{Kp~ zy-}y_i%P)TWYseP3ue-(lv+;D0dXLr)hs`(ffERo%XHcK%O+H93UaRLDBH=YRnJ^D>4-DWd;2w3^0%#- zy*?#vnQQ&bWrFFRaK$K3qS+d;mZjPY7Yqui573Wthlow=)>3I5xUEgp|Bs`$YLaYC zv-CDII8$Io!pXzc1x+)fnZ9UU$YiD;JSJUfG^3sdRdvyUgvbnV_RS1>ylzifWkk5| z{e3U2^{n5KhRMo3%*H2l^34teB%VbClAwSe!ud9N7}s(zJ;-4+5(bj5UWgAJ(&_%T z+)4VAWL2EW81Ajd&R{?WMy^2&VV8S>a9>`NF}Jl}{WRD%A~bLGn8IXyJXwQ?+P>4D zT#3kvgi)o2+=nZ2aCcOF4Nd`;5caGl(d%k|ay9h+y=aI7pM9|$0Gez93${O8)vg?> zWd1f?fUQ<1pWOGWA1ZniEN2Evil#Lbx391D`e~*c8N`&LZWkBcr_UgD$_ITv8}H6h zO~X0xqp^DwH^6MN9(JJ9PgONUZ=)Fr{koj6D<}OLpGrN%B4CU1ZmX z4m4=d_~=+byp$}aCoVgc8j@hqNiQR2y1;obt=1K}Va`pp_{U!^ECdJ=;xM^aEbyuYzB|m!Q4SA1Ja)pGM3PQ;qlIb7^k%thI%9dTb>Re|R(8qgM#{&E zzU-R`bV^SOV&2NVwMgC{q`Qw9p9+Ua;}Jt2p0HvgHfi_|gip6t99YC&!z;_~)cFM;_b;ImpHg}elI4hvQScHhTbynjcUO(hb zRu#^VhsMp@acp?CL_XU4gFL?4#NkmH)<>!PZd3+JeZ?7NcwIFd9&u4rm6;hU)bou2 zG-RpVd{dl$Z_ha@iK}OCZ`wP9EqsF9Dk~fw8~&b4^V2wdZ+&S+>-V{LOFT7(>a)SU z8HjQPYc6jFtnDd0Knx|2+hltuo*sMbrtgU>5P$KWfFjjD>uYhKvjRi<=*Z$iyl8|= zcNc-bjT76~5`Yu3I|}FD&9zPF2pA`tMFtIL#J{X44UT{RMU2d|p;DP!!i)P;%dkL~ zRhNL!h4lGJ+KOYBzGWr}(_~g-7?CcZv0r`Gm4SDPY_S6J5h25d8>AmrxZwIwXy%VL z`6$kO7n}Xb!->%ZO+u{MwBG<;;nfm$DOPcU9=T9StONC($zt4B3#MRT-smig`5YX_`Ug zxFnAT8aYEoFU3F}Jctz%mORl4NyHnzoD81NQ4W=>jTkv5of(-CJ3bO`+dr&DZM%ud zUe6!WN_36{3Sj*yg_?;9w@l|SutmTw>1(V9k|PXX$p0|Ue@3Ic3o00Dag7;+U#eiI znACWAMY4>-&hODjRWI;AeLPsNc_hxj#cB|0_RS(oAb0dyqf32J;psY+Bpjo{<$Ny3 z(G7rmM3ol(9T}c_#6TrlrdkG+Tfl*~M7GcVnL%mrV z>v}joIGqBAu&v#wQv-Z&+H9#$@IN(vY72HL3r`Hn^gcTIJrH;H(1x9ZUA4Uzb5ST* zJhMKxMG;~STM70dgk@G4V#}Ma#^~EIoP!#XBN<}9BJAX^UxeYiK^&bEqjNegDBK`# z-DqpREHaC@D0KE%x*MX>!+bEW|J31ULS~Q&_54yE1h9DQHhVnNR_z+eiEuE?RCT9I zU3-L$+RN9kc2xnscU0jU!n`*)J_WptFdUHW+h<{BtKw1=`;T>*FF|HP3890ax~L|` z=Tf4;cHg@4|DMMMIL}=QHqYD|;Od`=u4-K_BL7rfGE@<-JBSk*2>Kc~-0c@@yc{e^ z$(PhA(|>@2LS4#!UXl=mpLg4unIU~GSC0HL0^!j-ON6z+%8J+mY8eUhh-GPHGRI(> zz6qc@lUX2xd8%>;^N`sr?~aF~m=?`${`%$}RhAH>eI2ksKP$BavleUbi-~v4KuOd7 z$jSa!dE%N0mdruNN}VLv(^Py2YX4T|WpBWSXaU!ci4=eKMPDd%W6 zEPP8^(@gU`=!eO0*_UOE#&FN0e+>lo?g;Y&!)J4)ABauR64;vbNPKy{jGMA{h#C&_>+S2%W5B>2&wz&dstEs5OhOJBv(_zLXp{y8Zwe8xBn;-bpImBT zfidE&+hg{~98wo4J}Pi?!gVQZs}q9X=Hy!AG1h0CKg3t^f5|8#i%__osJ|;{V?bjn z9dDStKjgC^`$+p7#I)^E`6Bn{t@S$FySUzA5J>RA(|(*HgWUsNIY`Z{U=*IZd5&g4zqMZm7zxfsiZG(CGGkx~-WRVC>@z_{9#Q@$U}0xcZz$~>>-M#9mK*o)PR zzZOxULY}dY^lR*>*o@`18mXk42jVg2r?xMLzm4_yjgEp2j2EWm$MY=O-N&VeoD}d` zk|-2i8PRjzWszFNI)w?xt0js9e$E8Twfk-S3@O@+&GVp#Uz^=EtHD2EN|57lBuaFSoRrV+Kut31K~f^1tF9xS8{eijBIG0W#Y zGALU)vvqud7WV}w%wWKmcxJBaCwouvBGV$g7zn8M$j?b)Hr?IhP9Vf7uIec|t~@BTp`L_P zz%&~PF;yg}8D_;vjO`uvVy{#1m=7hw5E;KmVY%$5kkR%4UY3<&A z>CF|#9C$cJ#^V+aMJj#QY~e-bINpS~$eknd<~RXcT{$mpEBHUr5t!BV<(*FiNtf{$ zsP+N{J_oEJJ3h%tqOXFT+rrg(ro6uT+iylAIZpC1U2q<`#9646gt>tYr4`o^!|cgG ze@~3IG^sMM>h0B_Un@{QA0Yjarjv4Ql7$g^47LDUO^-Z37F~kfL%DGlYS%oL9{V{J zkzGY|=uecebiQRfUounjlU9=&hL8*Sm>K$Ojc_nHqv&-Eu8^tkqP{5Q?+&>IEe`FY zI(UgnBSytxC>#2WYKb8kS9h~Vjl0|+tLWe@FEY6Wv0dBDFk+;$=k>W%Alwd_Zgs62 zSFJPxt1A@v6o7*2rSr9A;!0{$T(t2k?FQGGohe9KrRg_2uvz-O3};qGU$lStyB zvrAD_73+mve?%>v!a*3@#&G71ro5izq*4lo`1?>aCy=ceA6uh6;R@qK>Yb>2eB{;# za18~76e45NlI_z28YuCCG${`2#NH8#Gwg?-BkaEh$HuJQDD{zwAEOB0a7>Mt6tr#P z9m(crNh#HYoG=yd=K$R>y3^M5pNKW*U{}X;C%m5!#f>tO?H0UznYW#nOJ+r>cTbUY zQ#}t|;kn{6V7kU<1r<)KcrTJXD6Ok`61R7wf4<)tSM(Qt#|5TUBXjD__yFX-_t$&0 zO!9aU_x*IzgJ8XGc4on(?|djBaL5qlQ65~jBuke87A#asExI;fQ^RS}eKixCv&LMC zHW~pF&x&BSC^-eCGZP^u9E5TSi`rLQ?**9wi#DkZQ0qKk28uI`uubBr>B-2mi-|(1^%&Rch!a zP=PX70JZOHS|Tj8sW4QVLu;mJbImb{6;!qyKI*oYrP+&&JSLY216TdD##i4D6_*|G z)q0<)OkXuK=q6=O@!i%ikjPCY3|i4FF7)M#h0j=P{~?k8t`KO@6SuAr;yAT_bpY39 z>C7EAdLyf>8 z@FmG(OFP%y;QsjdGZ|JMW5 zs{gl%6_-@K+Gu~=pA6hDlm10rofL>~M}DAfRV=RLW1?Cvo;-5^`dyLgi&KpkJJWvi zIzo*B#XqVCCo`uFfqEb+gB}%%5VGH-j$&f(BE@2$TP3iKG4`BNFImy*@20RHNTxP|KhGc6OS1jW3vAWdYP zG}E!9aPc6~i}BUq8^o37^`@bbb|1)i;0}motkYWzqJ|w+N)>I+I zl_r=2J7G*H3>@JUR*qGShldYjNr22DZZ`1zPh^ZURt8WDqFW(e-LLDhUp!hy2}12= zxVl>@I`GSClVtM$VRz8O+l+n+RLr1Q9&yx}AchyY|HX@M##cI4D|!s?ZqLdG_P=IM zgbB3?Oqp(0{scM#xrtqq$_Ek~T|j?BxsvNU1n;sutIS!gSM+AiId=WK-Jt_5foQL1 z<&i#3RP}RBVp7E#lDRlPR&ftx{ebRzW&R8FlmInES0V6`PfAsX$v$$pXu|V;2I36D zOk5f`B6(Q3LAOV?OpIDm+Vyr@F&+1S6>m(f1M><+IUd~t>yUadPqO2Z_eVUe3QdRo3T^4Xo>X0|-0qNY z_FoG@iGfxkhowS{7v+BWBXx2I^CTvnSXmoV2W#NO>M z+kiX~q&t=$m8R?n3QNih;r1uuz}bzp=GV{r9|91^dIbVs{@+G};^12kP^~8uxlleF zo5~PRAB1S=$GeBxe~IM}SDVYp|2u6f3@qTE>h^6oeKzRNT28o1k-KZAAv4c6T0 z)(t6_Y)|L#cwv#gK@{lfe34lzTrQi3-q+1FGp@;3>NDiOvN>tRs(~anL)TP{r0NE-jBr>yhbq^AFPVF4+A7Y{cgL>@phd@&-f?VDq4(%2;M*d zrrL!RoXbkS@mkZ#Z{#%Esd{YH>f+58Nw+79LCnw5BVWopg7C&|S?McxpX@;*Fuv5) z!SpR0!QfZnud82vu`Cf+Irq1YCEd{e?2Q&`CBo4FU?eKbecRKoml^|Pz;j`Qmgs!N06LW)b%3TcyId4G6O@?#_S%Es% z8f_N^db<-ZSA~fZrDkumGeA<)qHS+DDDml$gwpFYb_3iANqj4X0~f$UqOo)vUH9fE zlB9}Pug&#Z@FV?rCM92Y-NzHNRB}cNuC5jN3&ia?3AQrw2m5f`+*b{#B^df!&0l2i zp$qHZfA4M!NgI4oKSZ4#A1v;2{xD&6>H_X6%=^0jjX)c*aUVMqY zlHm$T;)2vP`(r}!?A4W~MC-SDozw=4Lg4F2sW#^$e#s zB5lJM((~As3db>*TG7Whr9*i@ zCqh1m!2!(vhH#XmHNDJo!uVv_sQ)~3D`tsF96T))!+^$L{V6w~QVmD(Mi6ELtYV|7 z;-K|@233rNrR{Gn@;@M|o=74?vRo!0M~9Rh;|o|H*9{370%|y|V(rON5Tbo}iCi5- zFt$*H)%WG}ccv9$53s7k5l92&fTd|9t@Czq(u18>@3&_LP1A{s(O1EMq#unme2TOS4XRxpdX*EH_eto^7F>}XEx|`R+hYKng976u^nkun{<5RJ`~Hom z9E`~uEr^bENheKX25NvZnWn^{y2CJ|yKYN&+!@DDsdf8#${(m%t^}v6e`Q{lL_p$y z^iLk{g}%J*tz>}w?V3=<@$x23j$-CG#L6F8eiLtze?(Lz`+`2(Lsi6R6^M(=@`NqO ziz{U6Z|kDxh}(g=9KARfJfwKI)e^NP6S5XHE7Ah4Bmis%y`x1g;0UFI&J}-&!vp;; zY0x_BOMA^4J1VR*a-oRgsi!+td+B|izT~xqo5puw4SYX(C$9XfQ`({D%($*C?5bl^ z`H{)M83e8Z0OsM4i^p+CA1Kw=EayU zLbJAG{Ob91(w0HJT@+vph@wKE2zuC+N!Gl83IuUkg2@bg^|(>6V-QS%B<>vJjSiWd z$Hr+}b6BW_o`_mB>kSxv!_jxyl{?B0VY40hOd@pcn;-7=T(QVUM8m*n%kGFs$L~+3 z5OJ;PU?4q!6r#BVQOs^gMYtG?qhu727p>{(>ES`KZT@d*5@w^)ga_T(4eTc;r;0SsxK z{+_JOS(ndp9yVLpoLDFKRy-bUG7b6`@mA<~BXeHtH*j~YNNJG3AFk1DlHRIh+nd;V zPa08JJjbrL%$`Ns&$D^9gFy}Vh1{fWXbyHhp;oGQ5=Q+JvS- zK-HKzwQys{kpcI%bo)7)41wr#*+2?HDPKaL!Z@u3XjO8OT^o|xDB6n&ZUWRYX*16i zRhJL@p@+?x{~(7p!8Ha3&-$eiRP=)A=39a8uns(!N+ifn12>BSme&JVQOam7wL*(& zn{HmiRlXDk?q%VjH&}K>Tue^udb&vb7M)pn;i~HIS*?nrFyLx&5vsciq-~5LRzjNocY1E=KO+8wYK0qRf!IgGWXY_2iO@MexkX0!n zbXQz1J^_;eiRimFpq^p9>%kK+xM;e~A5M83=f^`~PN+Fj4nk0LM%&-E2Zuh3CVb)< z)_vj*?B~u0?!pWKerF*07o_&K%_4}q^lNX4EI27!MnNMnUp+mc2F`8xqEkweoaiHBQPVW4p@w{s@^&nr}0eVNEFK0$;Mv~3LKVyZ&Q zwG+!TuH4h93}yY~pW_QGW7LwURrW1>%LeZ}LYC+ip$!~_tp3|3?UcWLA;0Vsve#(rGV8WmYfXIs@@?A&RkU9fMJRfdJ?c1|#1DVGDo#w{Z7 zgz#BLY%1`Db$@UK;?V>)}ChO>3qjcjVBL5m9 zGQICCY&`g1FAeG-bY#;tlg*fok&@NwePg_--+S`u=-N?BrUBKTl@W4@MWn#4D3ODR ztr>$}X${NF2@*>g9wdpD&qL9GBtuEUL>R!&smB`Lg6cwMoBj;l(C$BSb3{5_tvJU#=WJcos(Hr`G zSaz?6`ffjf)Arz-wXE%iXEo`kX}uc}7O@9}1U=JQ4I3oOPD~*t^yc^+$IRQPP@RSg zUa*WXJ0Y3BI+Tc_jNv?Bw@Bp2!BMf!v)f>Yihq@$#>^t1!^nk9W&;9%h2fFAjZl4` zUv|zM{L^m1h(eIF#_jeN+;FTelTc*yPr9M6r>TuF+!$4$u_ZCY8HAZeJITKDXvy7E z_Vh@KHHFxAFm5&lTw!KI=c<-1eK*j%8~KN4{#2r(ER^L$J1O3G&8Y;o2->ZhOpM7bd5~#JcxTO*h) zTRcB~+I|3Sk8A!fST7+v=ow*g45WGkNDvHJav`tIsX|czy1!p}p}C?>`6*Aff%>_6 zPb+{2_P)F^0HzUaXI;CT&23&RFfd~ zh~9WC)W($OnFR2jQ@0(f0TJjzS|2pCnoz15y8A_XQ#no?5c z)6Nz~2LB*rP8?Ms#+48rLUH-4xggMTMoBw{;*`5QNZ|9P_TO`))2`aHS&TjzZU`BL zwKBP829Cy+P|4ApWE4ie;@o==UAtv`_1dflT`VShsB7K=WR_|~r<%Tu`?4Z6QbYmP zJZSP4i~RXP>=oS|<6ZK+1l?{7)(4EMsju3MZ?EITXqW8{K2YMrzS{(yfvKfi6=vr> zH~h3-ZLLd$Jk8GyNTx_r#pPf#5qa_*5PZi9 zg51cGTVPU?pAt#hkX|0{K#;e=2k#bT%)Bg!WsfQbc5rG7Phedo2x zCEpZ|=-^>O9BJ!@4ZARG7QG+s61_WY2HkZ2;WTIzM>s;7U10Ow4aVd{W_XH7$D=mZ zNNhsEf8I}YhZ0b$2ATFqeGBccRBlH8Fx~Bd`z6Xm8WIF2BCjt*>1wQ7*Db~sS>FQbQzpIu0`vRW5ym+R6$Y(+Hojpt zZh?9U5(t4K2JH+Ax1)=NfjB}aMHZvBReucRZ?(e{WW|fuT9&P2XogIJl0a7#_Pjif zeEZV~;}C1WzEBWW;+ign%tL;pd-o~L5fCnim+ayHBjqBehxy2qpYGEa zcHV+sFOk}7#%Df}mqV=o29S^Irl{~hTBmeaEP%c$Nd6ndSX@|awBRJ_2T2RjCji5C+X zHG-u9!!#|Z93)}F9CjSup;Cq4dUK_qn&Ebl^`Mp1fjGTNKqa|K2!KIgS;$&+`GnWT zKOysiwR0BC0a3yPD4|kLUdGk#Kn;bc_wfZpu+m2#c~w5u+BWGJwb){62hnk7!WE4^85k z8IhG&rkfcT2{{ZJ7hRVZYUFgFMZ z1j!gjq@3VT`FicUF)*M1GD^oQ1@YVw-{9-y5_>RwlANiVZs`r<3jlNhoT18r0dL9@Ex!Bu>PI-w0vt=6lmk z<+s;GtdK-OK7lqpAo+RemFJ{%HAtC6(9eQtCf*%$&R=IIb@Hu9oZlWm9pRXiCaAJ( zbWa(RnBdI))3~dZ+2rNykMDG>OM)95?Hc`n++H=Ngf$6+%*CqTS;ETpvi`G2vY)|$ zE+J7ioO(mVR8}Q688&s1l+vqL6QT9$>j8nehI>O&nTX7RZatC9cER|Dtr#(P&8Pc) zR!V_(O6&LXXu$ySh`bC70RroO{Kf+(rK!^NjeG&)=aFuzu%>@ke4xlfazzSI_{@T( zbc|8hB9cE3SD@MMbdr3;(Q9j_{QQUj)nlTqw4I%02NfEjog{-%EOL(q`5gT(v#DhZ?tB|!$c87Ono0bLU(}d(`117O#z6PT(9~~sjyOF zT^f$8y2x4|+l$}ze!nZxHIhDFw#3q3%vIox8tftCG@KAmjfNisi#mPrZ|}Vj zYJ^B^SR*V8VVH$7gApF=_{1a7V|cDpPNCTvh+3gJi?`OrV~&hpd}b`c!gi=Tr#?Ci zG3XOO=%|0G@Lgle01(dJ@TK!%+8jQ}i!cA(w37@N3HHR)pw_Otz61E#)zs_P;r%-D zSenmzSbzi@r|RN>q@HP4dc0ZS9cUM_=LpHjMG{@0kShw=e?wNZK3uaEB^%L0@POGd zhWz0Fa**77>SFC3qBh0el3W{UVt-7b-N5$e-Km`rUsl~Exb=EQa~d=eMq*(4OV8Fn zpCCvf^k;W^S9pgLr);UOzB&6z?W^x+Wq-SAo2}9Lp^lfRvsOG7il4PGe5c3VrY+4T6Q)U5Hqc2HG=4j?4D;qGdGB=7cx=eOes~ps2tn0JU z8}q+1PeWASF_>nZg4!_Fs>nai>*2whh?^mU6t*lKd3B;rT*Vi1%(Ul>G)8pHppR}u z?A5_tcn>`n<|rV|P<9vhnp^)z@&15o=uAoV&<^?KnOagAj!)06C|qCMs_ZI>;gsG& z9ue&#ChXZl6z*Ty8_^C2G?+}hl_;H;=&?kmVW~7J6LgUv3~PAIUnu)!6~PrUa`*cpD95!?*(nQEoP^rCa<1 zJ>0EhD7HCShSKDV_JLC3K_v^o_-d)kuP9jbT#oxIYW6p1dth8u@nLX=zshL2#_*pun*9&ML+m_! z+=A)HFO5JI+|`$u?byZ7acI!H=+Bk?r-S%fIibI!rzNrexvL~%xpEnnGU)je5p9Hb zc7gppQ7ytYcuEij&kU&IlX%b%PtHtr?}`(`)F~j-#;NqQ-_p@}%(JbFxp}$rRuq4F@6_)v* zjNgHI$wlbg*X3Z<)ynp{sPGj6CLH-dxge?%tQ1cqR6v3B#q_w#1{>L_%90Sb3l>Hu z^%{V=%<9Em%+!f1NFo@xI-TPsircHBsumv!_j1=)gF{;mU#o`=8`@4W=NqT8F#c2= zMjjxb+ziiV?MYw-7EWH!r0F;lGV`1A9zKfN*da|ESS-dR5bFTAcl|C}$n6TxyU>~C zlrL)7(+uc>2%c%YY36E7XrD-|U^SP*8WAf_3S5$^WJK0=qn?Kb#MNpdG&)s<1S&bJl@|L889*Lz}roY zV|ZL(xZ0KTFWFM^t|wA{NUpF`!9z`zkUSBEwCq$M{Y!VVEs`DGc@-#WI{!lcVP3Uz zu$jC)5BaBwn;RGq8g;T0BHA?|Og-sq5G);MD@Yj4wi%6QyYg~*9N=^zO`>HIgJ3^Ink*=GO%}WU5_q#GwGO5*sA@C&Mf+U! zo5u4i{vC3MEy>P5$cTY3w=(imb7eKS``rvl`a!YJQ-7nQBtd`!keSn6tmIddF?j&- zc1;OQ{@uLJm9d==I#%Sut7%Lw5tAygeVvo5#5Ro+VT5g{rAa|PH!e?n5Y>cXDAtLA zCTqw20o4E?04XzvX!igg6im5n*g)cy+nvI8Uz1bzmo*kwyG9Zk~ffwNFtIqpzGavk&V!UhZ5^fetVY5F-x3eJOQISax(Gq=K z7+VM9%c&e)@Uc`+6|R*_k3b-KX=q0bJmchW@uK*Vz{C}`&3R+O<{PiG1pb9iO@bK) zifgwkdbYP9gF`V)3)6ASbT%06BdsFzGZ^uh%+PtpfMDtU1WKfl>x|8I%+Zp`WQQqw&RbmYZ!*`cN%RA~+L z49&M@ld4)bihUSD;wII~_F6e47WKjm6XF?WvJG+UxgA&;j%bd9N>E4IwkSSj^f!k3 z!VK^Ex)EyT4szA1v{m7&sUdyBI;PX^;K~?u3pGX_tv2%b7z-7UD>5>0=2@zEIx%+s z;>yG$Rs(Cm=3%;Q2s-SFVm2mjpWTW{+?)uGCk zNJgp=5;SQo8LUBe=$e?)0P=3D@fBA)<;(zs&Wmq~pF)S57el>A&qsz(Qt-!&e(@?5 zcONLP%-nRSW?2JF_s=2s&BWKnat^YWg!HLnqMh2Y`w>_B)(KJ;)dD>pubGT$1~KM~ z2jOv>kIEKR(W(4qnT^7Wwow@kTH8s9klN);JXq!49V07yW$ze)oEcCl?a z@HoGGaZn-oa;n&DWZ1r9$5zlk>C+d|RUZ7?ktvhWoRpOpJrc2y48_Pd8HkVwlO~zn zmrs~dnHOxLL>h>~`~W{PT6y>vup@$;MR_8veMUyUDqzN@5LcBR63;N>A%PfmhQ zgy&29qd8*~LMU??)-+oN82rWO4eEGl^4lY_(gCU@002M$Nkl-=q1sU2PDbmf4}Ye$&&Xb@ zis{m4H5h?``R*1EO7h=A;@$$zLe9{rZ#~f=pMV};b7a;k+OdA!d(WEu4K|dTLzrO! zlP0xggJ?=0-v!io#wiw9YoSv&*<~^fhRm$znI}K!t^$>~U7jV!R|baSb=lbiAozL; z^xk2{N8yJ@m6%(w=6UPFijm#hG>r`O%f*lk*#c;AtCM~$YbTucoaQ!CMQ^iGtv`; zSofe`lTEXWqocAo;KuYb!`U`R>rS)TxX5Bke} zvd!j1bSHQ#b@?PR2vPk=^pHnpj(0ljJ@$B_Bg|eWM~-t~HA;0P5jYe8)wPRvYigTU zKWlm%o!1Z`u0A-jV zrp%L%X9oh2F8dK08UP#EBZf0Fa9~$Z_d#Om69FA-s_iaUgZfxS%scJ@03l!l^{>mVQqdIzzt4YGMus)|tGPl;i-JZD6&4RFX+#r5k#3#^ ze$=FdtCU=XI8rTAwI_G^R?tS@k=q_t=+PySI2sSsl@%2zxQEW|LzrAu3#SFbZ zJ%Y_rE$pijQqtqq*Xa>^fk;XuTwh0u%l%tD%wi-b4 z*_lHQIlCVhTT-g7go?-Bp6=F#B41cTl3_T)bhSm{E)$qn=AL8f7f)D3z|cbRIjbiP z)8h%O`X*GbKdBRFo{3}|27{o|YMw;{G#+KI8ke}I5R1TwK#HuV&f0IMt|%yVT3POV4~4_Uf=-tL3H2f6vgMF~c2Q`Rkau>FUaR zsYZwtV$#UH1rXGL6?l2I4dZ?tJ~jRbE_QD6cM`Jd%kv}aaralvbD3b)^ssj}5wHlz zBr9`W;f|$-7l`&WBIA|3Kaa$up*G(`A$&Vs-=E`fR(^Fdt~z8(3IX=-p4U7_cvwj) zAlE?yr7Rn_P~lw%z9B+*q(e+6Du|0_3tay)-180R+VC)XaTe(_aGb50hbLKnGq{07 zBNmo+bD3}_8jGr!$i2s)oun&)gNzIV#0oL!GQxpa$W!5Zpm)WoP3hZ=0GG9EH0Z<- zRjn0IS$UuD-?O`&6+|xomuX)=X(~BF>3#Eyi2jALOf=3KIZIxoL}^$uJ%_Jh2HXmrMbIO(C)$p!MN!_x7D*UL$`J%!9@Gp{># z{-m<5l}~>ZGI0U{%i>|A5w4IR7NdcaJtKi1teAty@&jWi7P^j|XTlM-+}>3_eZX~M z^)IUebAOBSRoCW-EDy5z^u?HZbze=uMX_lFRD*F~Qf=^gqrXwS z?vzmS4ozheg|!?F+}Q4|4mIFP|MSOkUOILaUouBmTH=Uu1M)%-aLqmzmSNwungQxw~!^0S>YZ`io? zn~!`I1HFph0^tL}tbmSKy0Y5a=wgYRc=2KFRbKKqOE116a~hIna!W|4KOsp}Li?+V-def0H3R(jg1`$p7>am}!CI z%gj$B)uJ&$0Wa&c3=HTu>hMw+-G4fnmPxMLq2_Huq#&fbQ#U*#yGSicTaIincomy8C_fvs?=4V zCKt``NLU?{WF~~LzF?Kl|sBDuYj_iO}dW+XYfv`?ix(AKMrGfyF-DAKlU@kn{2!`YELe zGFi^_LFejGP>%|r9_}we&6VrppAi}{*uiX>+r%xMg6R~+s*vAmxOFGf9r4bPX1|FYaup0giaxc=VU*}K5>`R@OgKfOjHHmgzWjBPRw_rHHXV3Uy~R+j zRg8$6I=QLO6Cd_vl&rnGnt8()?%?Ho)^C%BzSB#Or%RlgT+Q;E?$L3h+pZEa z18#V&jCbtkG^U>!RkStm?%xU5?+Ttz=t7fkv#JomZCrraBDScJFAaAiMC=`#-sO*H zNrO!-bEgF2zz$G`Lxy}v?Uq;Le+d{>i3U>zCI)%fv@{Lr`!2omo!K{+qh#&8mVz@e zOXtu3&C$~Ks<{VFm}rJ14yoA5&%opD6@9lC0ImF~xcvN4nxkaJ^iG%xWX8{vS^9Wv zn@ctijyK**BT^Af>t;&Y-bs%}cDexptG(NR!Kl4O`OK=ujrnZ)Z90DI)3m3G+DI>nAT-+my=t^v@ z&>O~s;b4H$7a0vWZTTQw_hDQmfjg(|-8r|rIA-(&kVZPis9SOG@tR;9NYrU@tIqUE z9BqsxsRId7HPBt{t%gr+{8xIw9WKyy)30-o%JY}s6J=NN>kIxJ8j-aB1yCH*Xm<)h2A7ALjVmP(7FQo@ZW>2+tNbG;hOxkg zpZ7XTC;9m2tI;R=y~$LY1|tL?6U&|c4rvq-o>+$1vSBx${RjD7f-UOjr`=e8G1-J8 zgrY+`7QX2f&_XMMb5dZx0~MDcet146$@;~A2bLx;@k!k|y?oT1VNcSrTItiFmR>D) z{3Xh>J2}EsQt1xbIwYF&6)8o=`(kJY()i+Kv=$vhj^_4wpzj?+pWPfjk9-2!P+ws_ zQ9|$7b&^dq5`B1cxw0NNyYWg4gsapnKmeFrN#cTO60mCA}oyx zP9UO}Ba!eu|9|}4Tg)=4XF6p8Ay0jKS1Ud#A3r0}jhEi~fwj?bfX3n`LLOX>pU*1; z{Eh;Pu%@Kg!JvAx5yH&W%}7q-fD)Rd@(0fCX7Qi(yv!$iLD#7v04ZtReoD}0oMf^3 zqP~i?M}?=lB~V=6r{m>>u?w-fv3$e+@L3h0lcJ4QVQp)LOC1S#uxMT<^=N zURx{or+P+20zwpw(pN!;`PKmOR$F_#sy0}1-#2j;NKNZ_{qtEz&J4-JOU zJ&HVUAe9FewFFGVH*dTs?=q%ksWxW>??rzwF-i5C`(}zm$tTFM|CjCbQ_Go}p|<#x zI*>;n_ok&1T$-Gkp$)DE-l#^31it{$2h*)F!$FRpkofnpgePn>GND%=v3+@wX?)JJ z2LjjT%KhW&uiZfz{`6;tLj(e?NhqnW@J3LK5cvcC1L{j+-BWeB$eG`+gDJv5-v zl>J7!SUew~`*;zyD@MyClXH)lBy1{>=Y{n1Tr=!O``o-9;06#Z@h7^rBzMT5z(F+& zgkm-khHn&7yZzzLTKcnuh=T&i?|!XB=na2bFujddLD zSHYjXt9}6av>dPpTD@9l@>;fQY1CIYpI37`6@OO8mUg@Ajzf8s@z7{MREAYG1^Fq^ zb{6JqRk7XSh09chaL-&NqY$JFwWr~28s=XN-p?is;!{+g_%9pj)#4yN0<`tVE`Tf;ET0oM%~cJ1=bK|d)2Iel>7{^o}WY6JoEoKq}tEv?^I zs^kv0IDsMErO1m-Q4g(Xs!T5Q_M!Aghs{pI^emR%=y_8GZp=)VIs5ru^fU|_NJ^CN z|H!L#&PMm#yZh%4Z(XMTdoy>Zan_$Gg<`?v=8}H#>hvOdZ+{M%v79bO+iYA~lL(nx zGaO;J04XQ7oEeTIe+XDI;nc_M8$oNET}n@o4wycN0Bj_Kd-`s8c*t^jH-JyY^e{$l z+JA#ugP1_anh4#&O_~E!#pvZ6AJpdq0!i#+s&3?w-F$bdCd)N_MlVz%X>qT&W6hxJK}~sZOKtRI5`xuo=Q9IuV4N@5Jy_Y$Pghzat}Icb0Is96eIY{j4HqI9YH2^ zR?6+I%`+(ne!@qHTvqO$EJF~Heg6ANR6I%c}v1*g_&LhN(#%Gq)CBMRGf%- zIgsc}yY}1ngrrnkc77)UN2XYz{kcB_B$x&w{3~SqX@+lw-PHvZvmy*8q6-2gSr-~p z8hQQ67$w6q9xUsI5u!mJf}y$7-s}&k;Z#TJ(nA7u_akzmHYyD(_q*R`N~1p=Z0AI@ zoU-tn)$-6yVGzfR!f+1 z3w+X>Zak`kHbR6H$VjDuE|Q#RK`ahW-uy}KHbfh6&G-W^!4gl}jI7z(%Q`7xYH*1EME;(@*qhuxxysW zG$Uk+(&&*qb!W7YmED>rPgLV^#u#$u9mU^oAx!1l?W^|vlL;NpF|VdHsj`fZrGd9f zZAABz9uFm1rH8)%@Dzl_xn_w#MIM_MwQ62;yMXA^{z4X2+ z`WAr^4vqBT$j--$GH1wNZcEr$h>vG|(GT3E_>2S^B~Z<1Wg>3-pfH=%M=#`<=j0Dv zuE8i3>J@q8wmJNQQNSEInjCE7s=HjS+ZYUOZCui>y)=e@biS+pr+Q}$w=igNw~-r+ zNKxKef^N%FC@HdyvUsEx_ou0KaVg)!bYuaBnnSbjI;ys>jPNl}(C z0-OND#{5}-vEdJf0cq?Jq#-GhY_j|9H=k6wYUjMSS7|;CD3Z-{PSxJebKlps*1~!x zm2yvt|L^}6sF*D4e$p9+_2JLF=Q;h^oA*B;34xV=vQ1z?qmtY2x|>)XK9Y3KzbVwJ z=VjX7Ed334j&jtjey^D9aA*_XJtK`x^bC~^zm1j@qAv~G=HT(#PoWg)VeG;={M0%PM(rej}$ZsAW_b+c{ErEMNFHIFKNC@UQk*kt)C;Vt>xL| z{L4xDDd)zur9k-`zW-^G9+eq?xZb(6V>HSGW|VCl&?&8bt=^ID)o+_E;5%K6e-U*W<>3-E zcz4?tTl{?wJ6>g{d1RqDMW7L9glpOb={OcPSB-wIM#r}ILw96W%F0gJcj;hJ@5BTq z&Ttk7Tzm_H;n6tbVGhBo)G||mgjAVEa8n_PKJa&8os6v8(l>8Ru28hD0YMJkU~>MD zYx`)dDku5Xec@ht>+RksHg|v@CtC1!LT;fy8^a#ID+2L$d4$O^JV9XbjbThdr`$6k zD&@*#2c~9v^*iu#a)x12zEw9`AE;YQTR2KWHPf?`xW#TR7LNMUPyEPW;tHYJiOxrm zs^PK6Y7&Vn6%S#M>5_Z2ol8?%`CvRMJZ zsEX6M=vER=G*Yje8uw~>G{iu3`!%p*eO9J4WW9o#gNlMtOe0F-W4=aG*eGL!-dpV}>@XTg*&aA)a69_J~ zL!Y^wRZ4vk94iNl%{-#voSF5Kn8>-FDO z-q)^UNdN0uZ}s*uT-m-FFL$Gj0?SPL;q5)Xf-7M4H`Aq+nlKDE`uX48X)$O*NLxD{ zyw0&X7n^>%D{s6c&pZpoCdbI+cljaL$LBq0;slM2>W<_N449qJr`x77%qtIfPNMTWqLW2OrC_6&*xWEEcq#)VE1!AFWPuRBoL}HE4Qq7 zR8$gLch-aZ9KK=u9FemVsz;^K9tC!ut-0z;iE zZp>?caB2#P!Vp$B@-^CRMI-j-ItCWzNydD$B-N8w=F@Sco!+nq_ zz0BQ(nNzup;@z)4hW1ylj0vH)$S7gz%dPO+8fFaLMn=R%hH-OGJ7X#&J@tZa3-2hW z?ECTO&jDzAG*i$%rIW_yvMl0#dv<6L9^!8?z|e4*P|E=so$3JVZG~wF?1cfysA>(# zLt})qcgr*g%K%vr?!$8Au?pWqj%G9Nr{ zxx#qI{+EBGyT6WyY~{DDINUKKkb&vId!Plbi$FvUX^1lo&n_=Dl1+8qS22HlzMH$8 z1=Z%^ia-D|3eng*kM^;8wUes$TlMTxmH)7*k3{HNN#~ZX@wglhS}68^^ajVDev*|N zr>0&=DLRXd(}t5?Vl#aH7n0BB$}RSgTR`@13CFWOwjF=1u5ltIQkm?}`kw_)<;Hnd za@;#~t!uDDh6vea3X0o{0^=K>w=~L68bopw#h!yRkO7Zrv<%G_@@~!hf;w;K?d8|v zQssid3WBnP&b_!U>k*^6SQz3%8ac&2>>0ewnd_sAri%alL%hEAd78NS)dFy zNmkZC9Xb!Ar(G?s;&^^S&e&WW68~|d7U}+ohQVxlkN0(d46gmVm*g7z2m|N??qf%( zJ+D!puU0LRja-xQ^mUQ_teL;tax>CLc?^y;ip6JYPJJFRdil`ZNV|f5fMb>_BKZTug>5!gXhc%42ubko-#DtSb+RD`#22h)z z3ZKyC&qtB6cqp!1yE$&c@fs-bu$%;Loea%!QI>Bf%-|je9#Hp4+ zpwWCf@I{R=$q+ZxB3Br&hG~2l&5jiC?O#uXP2*_r{}H~M|LK_MEFQiz&`a0)HUEaM zs^&9`4qSuV)nvpw>Y&O8sn5KhFe@DbVRlQ5vm0Di`EcA$ADQD)jU&HD#b(Mb+X&CP z2X(^$-6h$-P5BoML)Lac4LJ zG?kHfwjGh_oluM3`TGq%-uYwe?W*XK$?T>lX^LTg_mTZFq4knfV~HL;x0(+(YBZ~0 z&3EGo&X5!0h|;V@VwUt#^-I~K4TK^K;30Ga*J6i|T;wM!ng)B;BFNc0xb9X91;6Rd zj^2w~GPiHbJ}7NF>FPCK#R57A-L={?-3R90bvz<;QALvsnP0gpl<6qVXeGkb(_tYg zzOftr@NsDK(|2p}>-qb?ve*j6g!`!M%#Gm(C*B$bY9Y@y-}AUOQL->WVX+uc2_qD#xB^$N$8NBraHw~4jyjM^d!`__v-jPf+@hY3|?5H3lJ$!!+%J8xVYkG z&0VX>60`iko)i-NvBdSbKt~#90Q!$Y>(10}!gKIl1<-&DZ&;QkyNpxegAsjbIJwu2 z0gSLbZK^k#{%zMX)fCt+lmAetUrqlbM=ow(ekOilZ(QUl89pksjs7F+Z*sYk2}DOZ zL(ZX}STJ)~eTGM{-h12(oOLDkU)Y2sb=(h9{RU`_uem)ZUwEwg6#py%Tgd5i`o1FD zY-FRy=!O5qFH+(?95qf`dPHS&!nSAL49#|>kH+!6z`x5la1Rtf zRARbvz+|ALJ4mY5$V{$4W9Y>~(9mX^kv{)TRG)k9YY4mo5FmCyy9QvDy6ct`{}5zY;GJ0eaOu)3XlEj9MD zI;t#?iMpr9@A<~okX%cVSgV7`8T4txr{fZywtb1XrxwjVR^T4tW3vJDsG@UFY~~44 z>x@3*=IlI+&rLF!9p$U#uAWLT(qVB{PI2)cK8nrnS;VeVn+wPuu5R*zc?}(wBeART z&4${>ahdmCJ_IPY+R>03uh!pJsr5(QvGWy{bx*0oz7#Z zQ+hSyT&>c#H+z?At6KKe%7X+EEE|!pd3e@p%8r|K=F zC^ypR@ll=_k;Ubx(vX|u0_5PUwox=v=>iZ_=UlAY!Y|C&8>iDtCq71(h=|}^ zBZ{6mvBLpj!XK?NU%q<$r|u+$qFGD?Nq}Tk4#Is-lfbdZZ=ZyNW2KO4j8LW39+{csvR$*#jjN zw|&f_RhQzL=RH`MM@4^H_nP#Ru%KSNS7&}URUbdn;3yt;a4nOV9QVYz{erz{aP-a* zNz^6`+wD(k1IO7GdRhi^uaeqLZi^LfOsbn_i?gxNtFH&mtlp<3Ytmz0K@Rd3S7K+~ zl5b2l#K;Zudy>YuAVIgQ(+nbt2%U?*EFcs9k`xp5i?Jf3Y~8!Ee)Gzc#0+DCClQ~l z7yLsh|1fZYuAd8s`_&G7b(K`-HLR(;0-+(7BgD6y_gSA|)X0LM6X4dL*r@U6&H(Am zqpEHW`@_d=yenoRl8fk!p1$#EC zoKZwgGUt?2%#leYSeNV7!^th~IcGA6%^kHdnXLq@fbuTs^fYMW>cB06ytDtKhp1sAE-w&iuPZ4D*7Azn?2_|TmB&cq?O!G=y=74ze@_+H1#R(bwfs0f}HRyH_nc)&t!u3xJh zd3xFPAS#;kTf?NW#Z?wlr47_C^o%`c%mUEv_C)0}E!)JF*>ljegOR76kG>$&%3jrn z|4)w#gE6ukFtm;EF7Z2R4~JrMA%7@l42B>Kqqo@_!|dd<(5JRDKgO9@4hNYwV_46% za2g&-E;&OBT>+=Kp*O1Tx*yEt2N_PPK(SK27=7fY^B47FE$Ycf=^UZT(5{-{7(}f6 zIGpq>W04iQTPtbrS~@Paoiw;&&dh5~D8jlOxW{rhy4IbY?atb**p8Olks!kIa6lOJC%<+-47~R27d@D%gI=zrZcH>iv)~S=9_%nx0#}8 z!+CUX@^XiM(T3{x_j_hmRi16ozv&;X$a?ln1{E5mkMjHqFh9gTl#oUjo1#P)(Ed^q3C57jN|baXKZ3$Bn@sowXgYvrXwifWSqSsIl!j z->m`-M*$H*^yzc?9Soig5LG!e0Zs}q5Rs^8!6J3Jcj^laF(gn+TGoM+Vh9*4z#97*aEl4fAQ`IPj;euo5PG!~B^XB|xK_r<{FMm;CR z1nU|I07;!?aNuZH9ZUv$3U$Zs*!Ldr@6btR!#6s+3*lyVow%*O;&KmB(scYyB8jvD zQffeWn#&yO%|YL2_~gl)w0G$6%=*<5Z%qG_0-!9fU8^Xa3Ysw2DHb9ZVh&weC7ljJrBoiHV4bOi|vIJIMK(wc4Z<5>nqJ8 zYAF?9o|kWS>lhel(fL*zWJgC8Px9T#XadJZ{&s|exrCMoVoOVpM?Lp%gL{XI-Rjfe z;f{@)u?g5Q*JMlGhQAt3^a>x{^70f!4J#4PtckA#GN{gBC~v|tQ>z-hSuLczgHPCF zar%V!5=@1FWhnT_^Hu`#*`4+JT!uw9w9unzkAD&e5VNIdii&r~q1`Nb87r^dI~~V;LEkJZM&EpKlRW_YAB`85 z(zB5;VQy#0qhCL~JG#$saR5Y)=?1{(Gd1eb9qaDKg_8ELw;;J9{5|9s_skJqYGFFD zr3+`N#~c6TBAqZw>Jl`H;^fS>o%@7GrUqRD>bU{wvcVZc?Z;5({GwpX;^#?JaLbWY znX{)A5>{e(^dR<5kv6Qq?C=#iI->y; z)7{dLE1i`#99bAqL2Mt&ZbRg-XG_~w(Aom+3uS9xU0cuTiaq=4?Ea08>VuoH%(h?HG))Ikuh0Z4uLh z;@++e2MaHUh9*a+1C()np)K#^{x|Uo(Cg7V z4JwVypCwBu0LnIy#<;C`fze7+lFxE`S0lM9^||$oUff4w;vMxVzDykH7z>u=jf6um z3}M31WZpJ719fTV@gxknhHLz21opavq53e_34#;@=M8g}rY{?#68#N_5BQhw_GxuE zdY*lZ_%|t4GMQ9YE9~t=*U$*|8)8c0*ZuBZkY5k7x5eI(U+@|gzm~an)wVXw=4!xn z-<)or)}neQcy8%vVt+u#PP1QgFmJdh6Fh2LU#E0)y34hCW$)T4jp`J>sfc;(m|A8^ zUV@|>ubzx0w0e`9v%Y$LE&ugR$BlM0rV-z3S9b--cxa0apjI+BHY~6e@c9szR-+9@ z4-Eq*c?`Ecs1?)!G#WUH0WXj$q798Q=pP+)YK|C?+i9OV+0oX}^gx@s<|7$wlcox{ zC$cuRK9)64tPh6H2zbSPE|+9`GCa}~W=-yUjm}@9H^XgPT>Wn^*Bn!Y6y{xt!z@@9 z+xx=ES4Sq;7>ULO?H09Ln9lM~t^KR--yM;2cANr*Ae>Kje+|f*J+ARYMBIfO+#GrO zfU+Yy01vI>$NkPqx5+h6OuN@E(g|I61dGgMv4oo_$SqeqoY=2E_fU|*=$sF?^Aoz)l6HQl z>jH`1YTEWyudJa?U>uRRWMw$h=|`1vuZK0(w&e**Ju~c#u05+qN|K zJ{03rU?#~E;xwAaQml!$gY?70VYk}{weS>;%2m-|CH0nJOf($HMxOIT6~$@&zS>Fi zUn~DghKczxO3u`rr|Tm1^&;HUEJ1&Y!`K?e%mQIAbq&I^3>lM{9Lm1!y(=rfkjVv7 zP*RfM<2JFVEz?0TA5(Ly^NNH0h!o`iNwh8?$(SNJ-xn*>urzZVw=251=tH_IaAA>7-4~dQpC$2$;0;Gc~)u+F6*) zNHrWJq0hueGNF>?=9%Y8dR$VpJfyq5JibimB1HATFFfpUbbG=iQ81>o-BUNfvYFQ}M%CT{B@?o7p2mt8nppeiT`m3I~J{n`pKKwW&Ovocd)xNr-JEcu*nJ_^V zHtkw4VSW1cKI$YE$PBne1jJmSM%g|ThJUwvleWyL0=5^~V&3iueERVrl7O?dDuFqU zo`gggc~Fz{wV*w1jw5?Pswe@cNfWvP1Qk*FD_AFm5jE6`5kjB*u@xxe7c;I;rA0_% zZQ>$?;;2wKP<;G=mF|vjU-*RX8hJh_t&coKm#H+HqM2WrVrCKfolMMQmSzn>#BCxs z2K{BIE_0AV`JE1tyGi96#qx-Au{3sA=WXQI$pg(q>PM!8XN;o`C=6)Xr<3ZC>2h1S<=9Z?wT-zoLkv1`Xe-|J+(J-X0l;Ck@Vmh5LB zBlEL~Io+8W;R!vU{p?x6zR>o27>!`kn4D5!FF3349UH$qP_=5upgXg~qNixWUU1m*@N5pN*n5~?Dq&A-$zm1lM z$6W9<=v}!GqF841u_F`4TE54aoqEowLoq9-_E5SD<`!TI~S1?edLm z4okB4THDjR24x_qJy{=%n7%3)iIgQ`q^gVEPFlTb^IS2VW8Ku&E)d1{_y@0 z-)&CCopWq;@y&P@OvsFObQ2~1c~txraUwQ{%KCfZ4Yxn+4hGG&DGnv5QhY}f<@s0h z{dc{81;rQZwYf_=Ug74WNx-B`4%k?cRF`Nq7_9PPQ@JwP%gVipawC&rmUirZRxJ_1(nUS;snVp|#a@xFa*0W2 zlxW1j+F|Kj_hK%vvYC7&PD(V2;-&mmVFsH%OX}9niS%2%wGI{rw}W4&VBk`Z^$Kf6 zML5>Oz-)L4)KMyX8Otvvt958lwY59#9gGylf4io}!+5(*s*S_(n*(p@e9`~O1ZL~Z z2m@9JHs7Z~Ka=&q9kZz^>VfzuN8fxQP|CmBgpl5d+#3~ms#LERh5Z;ql zEMrj#Pu4xKrH^e18(&?uvuFJU@Z&NE_RQX{NmO4?BP<-t21SCM|O3K5fr@R=xlKv@enze+z+_P?K(%j zJCrY=!#NZTYkIL3^HWejtp7w`)SS=yU?an&;pNCQtC&tTH9=s`#ee7=S&Qk1$U$2) z0%c#A7p>w1Z50r{5E*shf!%dO`uR|1Je44{Fs*T7!$+d`a)f@ueC)%gH*tUQIt>k16X;Rl82VX<#c83xlD|sXel`v5H?xf!BeT%m5VFsWK>eikXZfc z!WmA_)?w$k4X6+`f}4#XPz@nLa)aT*cQ~nG4uQ2sRAyJF@VP2(@M5vYO3oWGsp0zt zhbLmz-06Kbhm*@I{lC6<4~C_XrqxJ;abz=&(q+;=l=;{GpO5(lbx>$Cdhp5>oX<1b zguRQyHoE=zA<$2^B;!eTUrg1*$^QXsl3hD(y)tXEk%c>txA+6q+SJbQ%!Lj-a2_8B4wNFj^U7t5!uUq_ys%yxB`bQqNQSYi(=$LQ zisEoX3uH0HX3*6sn;CEh?4C_}q-eI~Y%jPY>lh~dvx|0YeG#YEJ@rrbXU$f!$B+DA zLPq^Tfql`Ps9&SM5&m130xVZb->+14v6HPCPql%Zz6}@IFEql*6?^rvJv*Dp%VN&k zp|hR{HbRGZLX}Q_q@7qRx(%YGUcEMxop`8=^5yJ~xzDMn>@mLkZT^QgG6k}^%?O-9 z2$S+abE-Ixs>N-*uOG$WniDwNFjd?kV(1Kz1)h5FtjS4%)tAfov~GVjDxN;FxCLHt zhs2ijBfD{dvC&1r@S3*BAj2Cnf+WH`P511gN{wO7R9ExDBsyt^&%yQjWFV7GuO9*9 z;!Evj(6b2I{(vqFv3O%XnQYfNUg(Muw(&#GBEdxyV3Zr_&L_*D;z4;h|# z3daRHapBFI*n4HteDc;!$oYjf;vC?weuT~7lHy=2_j1L*+{y*u^O)s5R8F?u3!}eE z0Jj&C1w>Y{Mg95W7jZt>|0S}s9_Lnqe_vc}m#kGNwD?8vpmO@eXhp33zn>2eae?g6 zoHww=Wxq9rBcg3Ch7o7`Js*?9*3h>3ZP? zCnNQA7c+8KnGFW@w6yx)#&xM;H-5OPH-EP4H7%~QD@1K*HDC)TDv%}KPiA*Q${mW< zc*+CZxte`O*HBukuzag#l$VFq(VoJhQXdtYn+Cr=BAB!n=gyC2-QM&G!NBNPrlr7e zOwj?)aD5^#>5@My37#Pl!aspDK6{EJ=TR8OJTn+(55JUFKcQViBHCP_4zh1pqH_Uu zZ9A&Q+Gr$g{j2mx6;lRKJYKzHk}El`?;f6)=gvL*?PlsLLvxs9eIYkIK9&zB7j2~&5Rwgodx|G+gvw$EBa=1e;o2`JlAfpa z#WAZ!z2+C48nr6F!q42p=#(~1#6&EWRr)nVl%xPWQ5^u%9e>jw(~$oq&pLxyvusFv zKJ?}!!3RfQq%;t`CkAkoy^E&)XdSAf&^gzH0JsZBUPPGS0#Ooc+Ekph zLi}!37ZZ6g193meoM(e#`4+1wN0MVJS|drmBxq+I4ac_ozN-#b$o*hR-d-A+C63X- zxkmj!7ZN!~-uwEnPp0984FFO=t-l}JV@g;By)ltMZT-_uaK6``V#fV6c{xdcp_K0= zfqXHW5gI`z=WCIedP7sV?kP_xwA*1{Cizojn$#SU1))09UOemuSHIEzX-^?*2?5e@3t*62Rl-4oFuXN{9!Wxyq#f$ty&NrJE zhN4!ZYhw>T9>!CXJQMd1p+T|&G37}9`98h^Na*n>J zMlaM4dA3hiaSeJN?R^#!+XBHS_mct<0(4Zd)LCk!0MR&6XZ4wleT}GK^Z_X0LSDB{ z-`NG6Z#(~D-h*ir96^^e9}pj{p6n)gg5xdN(@x6$XRrE2{d9L1qX9I_lF%PVBmP?_ zP3=EDMy1O(v9t5imdmX%)po*+gIm07GF3XgN6)k$xGJgND zg1Q*$=Mz6%k4Nq;BIqakBEvM=C*mN05t;+mda8%drMF4yuh-(9S){7&KQ^^huV5H! z10<(yC^8zDIFE5hTv|p6Hs>fG8ge7}{Uu>iqr14@8r#BRiZx>EF^gZ-3NK#DY z`|@|mMN%Z%fbeuX)je*YtPHjlFWX$9+SJJ!JB1p8#Tu(CZRTp5*hVx^2p%%!YwDgG zhkWo18iH(&(u#D%VLm$ar_LufRqR6k%8G<$N~Brj6dqconEdU3_etOGFgucC;K9#% zU!Kb3iu&ogVwo?`N&h?*WLxGJdpv=JhfX*r_M*)s0|vS0Mv0yw$-`3WId3tQFJd(Y zP0v)piaqm)?3bMC0qS4EDg#0w{gWK(Z!rtsmKEx6wOGizM`hH%4e$3!$Y367REfz` zSHk{zu6^R=kl$hQa`H{Oq#pg_(;Zuc%ziJTs)!C4$8&Go9k|gwSUmkZXw;XWsavy5 zN4*1GD7p2=_oP#616pQ2^ZdKV4N+(b1892yKENsnX22YxD5y}g3T1yV8>^V*tSmPe zWqA6Y=>jSXUHTTOl-Q{tEVin@L3`gE1V58SVEaS8L#%PeIjc>f zPk_Wa_8QtQk-^d!A8;O-aisD{@U(QP%w`A+{jn#%LR`gSz35ali??$pL>Yyz-S$pv zNO6>|PmAJMXc`xtJd`Tl$zjhLNITxsoYhSB3`t_0Px-NEotfN3Ghr|&$KEb6HE($H z)4hiQhDfMY9&Pz827&+a+o$rce{S@EnX}^XhV*VGQ+eKVy~YLBA@!-}>^_y(d+&R$ zeUk}8UwSSEZfp<)wA6~oJt(|c%K}Sea!g(k!sB0q8JHESVufYW?;a?X@Jntp*2*FU zHbrNlnOcg`&2}t32Xpmlzc?Gp(Z?N_Pino)Cp9U?wSMjx*o$1$KFtNnldT0f$iEroo{vf`&eFumxZ?GK&vj`I_5ze{K7SNIQd9Rc;J+rE=lC z`LJIntirNf)Oy#>vqk?zQ`QMtJAFkT4DXIqsA_LC)9?19Idd>lw<=29>T*DqHB~*J zkqBv+B^ncD8EPskqx(av5arOuq%-q*vmMOt_R9RrW*u@kKSF-Bq@*%AVaSIlHE$fe z#=VPnAT1+4Sk<#6CfBEX2klJPXd4nFpm^4l)})xflAqvSHj;};C5`&%-1C9M4cAD# z+ae;;vc@BP=AM3AN6CIAZny?SmSdHlD%N8zaoCqOoIXBb3Xj!r(4w@YJf%>_@WYB1 z7Ekgpf^h-nC5UP7^=wq6!b-6&v&twoB!U#VIyL7HXH|r?CK96sK-p z(-cvuatVjLN{)LfnHa!B+55co=O- z42I!agj!5xdg8L6dP1})0ZxD-FQf=O2{k`pL5Mhm|X2SkndJKZ9f{PlvG9`ju)QUseC-YqCv@YXU>0ppswym-`; z%vfv|E|~)^Pxw0I02TLij0W7__TX_e|&%Nj=vmsWflyMC!bX=jLBU^j5R!-E*J_y z5M+6f)oX?Ea|I14?Vf2_I~CvSQ?QBwp4W&Oqk&M#bBf*yuY~p(9GNE{q#ynvxXUtA zvG)Lj?JFe<6sP+or^z$iL8L4$c6skBcEQxj@f~qf0f_ku7j+3Sg)H^5$v#=5x5|79QMF3W?qws7g_6vYhj zz_tjrD5t)!hkQ<#Ob56c9ztPh`Ol}-F5D^H_W?VmM7C=yCU1|$Zzd!h$P-X=xdxvi zEw=Li!csUDPvk(=IM_B4ASBX?FdBi_f)9)4tkI6zmF0NMbe6apRFz`fzyDC#&(?%R0 z9`9Fo-1QA@`WT6hm3PzqY4vSE`_$_9Qmju{AV_VzyU!l@$Psyojpl9%92u=ZIq)Cv zp{$A*%HdfT97Rgs^|{GQBLhi^lt5HHx})a( z5SS;G2z}xMQ@Q^DK$72e_=`!K9UD!ZgrumYa8Eh-WlqQRbe~_$t^J5ODO=WNFYSxvkAii?t8pHFDi5|#;Db4PLHd1JF9=RT?&+Mid5Qa@-N)E9pr$bW2)|kA zM^0k!2o~>j8_ySrq}28CuzB2e7GXiA%JOgdLdrD3fOlwnIs~QNeJBS+ ztu`$*e}aB!tUnh+MAE%v*^_##$Q9<<%1yn3EkDw~nLnSzl+n@(LHKsh`^2K=+aZ0>Q*=EN38^(VPA31jRtwyqt?G3(+!!Uo;3?a0)EI**`4 zcaOj{lwu?t54`ozmy92rh>n!ZEW79%szcb6&2%}VhRZ0hWEH%sF3VijU^k;3BBpfC zqOVFqVX^Lg#|EVM;ybzX!R|=dk9wIcH7tzVv%-Gq z7(uWld>61Ed%jN78<Y1JdjqIpkj|2|O~V3Ia3 zS9Gbo=G=>!=2go1sL`5kwCrU0@{YYUwfN~Q7CelbqBP+ntORSeP#s5(%td;%t&ZoY zYF>Y<@Xn&1lLZX(c^&fXrw=71>K)R>Y>7CLg`RbLC^^h}D3<5FYgQbK-ZEZ4RWZ|8 z*(5}Sp<^zB?#SC4&#q5iy4`qEE915US(dd3cY zR_GDM;yYvBFS&WUqYcIwde20;+T&yA4qXtMoLok+^+V&9&+_5p8SimDH29e`Xbn}oWrj~iTul<^*L{P z1KjlGq2 zo?5@hBc(JH-O6obE`XtEBGP>0G9-GR%K_7KUbC^y#(7D_u(XH9UF|EaH_YXMm}3Gg zhB{LbFmaEPqiXPMup)pnEI)l@IU{zq z3nYeuSGkYb`IW_!_4iMQv>GHXfUdBBQx#~lo~hYh5v_}QjKt4nuv^4dQ@D-@4XxjU z_2{~&#J*f{Mz)hJYhM+;&Ku@tK(Z}gz1f}lEdDNY#a=oK~ zjG`{4C!drWl=(1{_q==5Aup~LL-x3n!h7JrhX#sUR*Ke>Le65cI47A(uo-5NMSFmjT63YZ zf=BvRgnasZ1&V5KyoeV;x^Zcy;7rv6@Xl^wIdz4-jJ#7hf1yP`rnapuiZOUvT7Q1o zUmCL!_uEoTO>tumJ8ZtlSr^Z(L$?q6?dp`OQ~LBHn!CvM1*YuN+oNJ#_P5xOh&Nfp zp)>w}S`r-x*4d)nZP+ZyurDZ09Z@1sZ_DK`zy0Q`A8y_WUoogI2{M#NeFY2`K^b}H z1O9KcrK!)7UgYs9@?fB9Vdc>YTntvbuCHW>@{Tde|L7(u_!-u~Djw zed@YGaRL*w{zkxq!u#_3&S44#QMa^m`c0_}RrUP{5FCU|@9JO@f)Tzm9rMj4l9bP4XL6W) zU^J{&Z=+^Chh0WP$*36F=g3%q_4uF-v}vPc;q4cX z_hotb$XHto0Uyde!5OP?GEJSr7T?n{F>jCeIM`D_>73T3z(++FhWJirS8rGZo(#&O z3rP~;LR*@%-@vit()N#oU+cenw@!3qA*Hk#$0F-e{|1n6QNY$~rbG~`EM<$t`1hpQ z@bn@RN!RgPMRklRN*`a2^CEcME^(|u_T@@Ibj;eP>rJqvF(OI*>OuB|aqo8h zzOop2X6QI2`HU%WJxK|lS<>(oap5NWPJQP~x9;0>$ruqzIQCUl%55Y7jg!2eF%*an zhit&|)+`1z25&z^uWo`j_+5ll7xm_lx4rYHN;`Mgk5slJE`HT?CHs0z+g{(AE4{}K zYck5Mch!sE`l=V)?O$x^oyxt9#EdL6cR?R$$H8P^18>uFQK^xRAoZ2$ z>X zCvMOlMy`=>pf#CcQ9DO);dQ~$#%rPvz3&4uY?eT)pXYcrst@cjcgZ6Cpb6F65WKV8 zdu)f6(J8R?$kQ|n_5b|&-Mizef!}|6&)42X+M|?A50iT5XV%d|XO|#wYD}pG^u@V4 zb{(xrjwc0)7M>nDv|jS<et5}DRL-rDI%_G$Nc=$^jG2vnci|@ zMM0xX?X+O;L2FFwPswVY64CTX>O(wa?vzf899vgwz6L!cESk+(pCWyIMf@iC^7ymB z-9Dm2&(xsnP4giSjZrJp z%F;HE1*6P5DhhkRQ>fO>zToh&@F~eB_n0Ke=^9P{dV+IcN4{I!6tDl*{9HmPYJF>O zOwGrKea^m#z&SykT>hRhMeW1hgD0lELJz`}PMGmzg9@Y!BSg*t4`H~uNTYU)p1kY2 z!AEn(vw8RGB9Beb0V&O1W=LS;QF5*Q@9)_b+@Q`a7U)7ZUbY*{toSM7(MbxY=atkd1_&!n_ijwOYsKi!P5PQ{Psi&*%Gv z{x7Setkj#IK(=Y9R1UzCRn#_XeF9jju0*Bi4?jfA6=bMW_n&a2#5ly%)B_yZA!nFV z@tu6U2QniPLT9Hcj?PXO0I)iKA)Jkj)xUKA(B19$@YNG~pN%o%S4>856qa^84^Tgq(5RnC3vUEJObSKoWqZ=j zgkq8S#-`UxnUUeMlf3Ek_U>W3ZL5ipP|`D&e4tRpfX|oHPfu0pG6FIfvG=)m{rwho z_UTx|6teePEbqW|HJ-&OEJ)hE=3Q`MMi^WGU6g}Xees>g%O969UrXuL;Y~V>#3Hqd zM(4=@8q-jd(TnVm@`2Kx35W-u_aO}*z;~WlapmQse60@MK3Aq=kAQ4gcvi=4&)2CB z8{v+U{B1+XxEGLO(%Z`K1F-z@(_j>0Jc49DM=MS5K$_2&%c=r&&RZYfq9IJ8*5>(% zd@Qs(laQzkK7aDT!>?f}_t{5sWYetu+7n~1r*aG?yb!jI^5=u%Dbl29#`f9_%p2;~ zqzJphx;bC}%l?a-;OF~ICa931iQ@o}Hz7Xiru0JIUEYVP&y6D!i`AYM;Zd`^@o|fq z+ik4H_zW3EKZ{eL&<3B?tNXixmom#U%-4`w^5Q@dfU8Vf&9t}}O7wflvM$rnx&F9O znx0x49^MSPx?E|ro@9Z@^NR}t?rc%@BAS}aV@4!v8&p%+akU(E5VP)Ac)mqt@j82( zope6pi6o37 zXjU`^BYU^ntrB|TS3kZ-@HPCnq9NSE&C1)+DuatE*q527Y?4m&Z{#gTMSq*2BpRR3 z%-usx9{V64c@nwKu{8G+Ilof>%te@7+)gqkFUe+@@gCtP%tRhVY^NV6HFH?sUxK{C8k1Q35D8=yYu{_$w8CZ$q8iBTmh`v~QS9m%3thmjCnG`*rcl+m0;M zh%S!5=;y+DlG1;6+^-Z-*&k(0hY~%|@bs6Z@0D7FO?QC*W-lE7XcfQve4Ie{gK;!r z2uu!^%^<-WN3g<73>cdtIpDL-rI5x@!m1}5wj8K46JA9{>2qKdEnyi}NFX>#MYzZX zOnRb-$?HPqR8)jJgFa|n zOB?pAuJwdpjvg8Tcc9%FL`oBGTTQ<$+F$lsMo3>*pO|V`J;w*bx{NIn72X5%DYK11 zfG4VA9NNxBX@H2D;$sef?_xIVhxokNE7#2o?$~pN4>31amfn{?D2P_$It{9!EXV8^%h%e-F?B@58utyYc`G<>X zU!(2-h09D5_prgR^CJ%h+Y>%L)Zc#n>Eqs^kgXn!V+XM6B}pvQExZi-7WUm}s8237 zIhJi;ez337QM*H1i|T4_zeUWnIaG_+%ZyaUdE;Ec8&f7BR8hJ%Dvkj9jo5;OSnAWq z5mXM0p&~}SXOb6?ePrWQkQnTkktaJ;-bO~II8kd!STfcXB@<8@*FQE+(lBJQ5UXXU5;lKRobi-Y*$C@gtV?v3iAdHyFpFQ)5B=U<*X2LG>DwU7ZKn^5|qekw^K zEoNt`aDHK(2#M6JbJMUPgzI2JAXl2jOyu^}8Z!w)CGCmJLC{TNhjLK&0bq(XiLRfAo%>VW1l-Bz=+f`7z@mKGk4KdVnT({bXl9->zOPPZ+&YoXgTZ z)A@>zgT_?F%>(|pc_{XLBBkkAm`_FOCW%f&denkY^zP&?COe}VC-?P&(5*TXK8uJ& zDqDghs?%?cAZz)YJ<$UDhG)E}h(%~Rx||CxPRal@vvBNp|vBgaJx6{iyM)Fho_58_n5 zis6l^vs&W-@u?V5JSXtnah9M=vW%CDZg}V?$+Q|aYlKq+wt7ML^4T9q zN+`|<2OMu&t{~(SqIk5(3)!efyR>jaFRc-OSS-b^Zh!g=W6u8OJ9&A}uvxGAx^M0X z3p~p$x+5W^CeC}nSc(2O93_uA2Z{oqk$N`hT_22r`60AAChs3UQHJ_vVM)cqvg283 zByac{)@hH3RAdas^i{eiVlcTaL}$1FlAOS(=HoIDZp#O>A8?twk&p%K&FDm30B_p?YYGBR&I!7ph`1 zs8BstFHBUb?I)$R#f$QtF#s+Q(e@7L64LnLADmlC3A$HCb0oydipPn?AbO7+rQ})2x(Kd>QT;`X>3?pw@?%e>>3h5bqgpi7fZ#vnHYB z)k@b_1DraR`^iWg5mAfC)XMJ-VPW`tzD=XA0^jB zlPg2*qd9hX0)aepTOQ8#cenrgZ<#l~HhA(-dYicz7HfKo!lAKu8Y_F0aGO$>FgmFO zDI+ONcBq?iY78FNkQw%8w?8V+ZdPIykIvZJyOY)l-(a6dBSCWt@<+1_P~P$kU~;)J zt&qY^$?>hIVuUM8j@nq=~*s$(RDW7lQeNkzdC)s`^o<6>kb;g+0J1@g;k1=6eC z2nf-eVez^G$yt^ z*6ZwE@}6K#zBnI5ZU8~LV}A4U6NDNbjY4jJ)&M#|k?e_M)4(#R3rj@Vjv}TYL}9kL z%B&WGa_eu-0x;h?1*@tIU^N&l$L;(4Rdo$hr;i?W^$ zau89vkvgb=_8exr$~}764TYbwr#XNB;KwjCCp?B*e_BVjWY2cD&NDZaa+V2f5R8Y8 zFRbWBeeIb7MzAPv%;sVCrd+I87V%&U>7Lvw&m3;N>2%>&!5`9mIn|hDA0Fl*+>%_B zCA-g~N$v4XF$RS+JDp~Kf)|#_V#hph32zLQpTZ^5D#&or1OsTRHtSz5_hg+MQn0XP z+LLpxBm$iGx;Qy{MBP>RKsF2SPg}>TBT@4@eNRaOUMNB+o;O!A$sIknS~#p*S4)^) zv!KLsVISn+M%o|JH83uTCh?pV8Q(Sgj)Qbh?7Nul$;Ao$o<&mlC-NUTX3woi_w~W? zcl14uMT`wAPROnALIsV7;@YS)ZxcxaY0_Qn67|Hx-s&Ge*7C{6Y@;d7B4YVi3`ep) z%#0%=vJ?6s7~55Ln7zL$#W785MfB7krRl*N==IYDVON?#b(VOGGcAn6YmA;c#Y+b` z-^OmIHEiXWcOmTlLWWIi8-n@$zU&m3rfBAUl}6dFU|Y^?-p`>lXb7pg!CxT=(kFjd zPrn+z|A~vhgHG>Nc|SC##PKfo;r7Jd^S*_KZnT#-QjO$ZePaaT6iFyTH=!M-@5ljXz?4xg`+Riwu)ZT710ZT>S zqK1!{Gq-PU_44rKZ@O{paYr?bDOXEjdPhvVb2#Ox>>3~OFX&u?K!t;nWiUnvu$5Uw z&d&OOjK#o`chx-l$7j{`#?tD!q3(;?aQ+G6G{lFbx|RI+^;Caez??RWL>X_mrn;Ul zIZL?Qm28;6F9c|a5KXCrcE$+)02Ps%Jr(38(mrP>-QFBm|MI9q0gra6gZlBr+Uz_z zyi9^&n%nF26&J%k+VSg=X!a9Rv3QMeQa|8OM3g+JhL6vFTuCqpe3F}zgSDB^zQ4#Y z0ZJUbxGC8<%^_&YGq7K1i0QrIiQ~12njspwT{rqV_<;>4%(itHHS=x*R~F{=LOscs zQtt7GfaGeKtxN%J+tcJ^Y3czyD2t#+A>Y9 zoM|LN?^C-A_lCN^r!UL_aLWe_BO?YqYpTgR2$}pJ44gw4xG5{q4dwZu^;JG>6l-JJ zss^Nn=$>)y@T{jepr?c`XOW;-&Rd7-<*#b_CLSQWZvGZDBH=R?Oc^foYKFX|NTuu*K~6IY2W=WBOA|iTLg5vg2S^W<{EEtKxD~mE0*m>s|NsQ5Nc@A ztj5jQN?|NJXb3E+B19h^GimDMmTy?9jZE7KeyZKs`DTY5scv@Qd1}fld-|4!0PgDm z&z`4`oJMldmS<)+M7Jw8`ZYWx-Un-Xsq=um5N46V2;HK%We*{Izs0H`4zq7~v>pW& zTUUi81_4$j4;Ofw0OAp7Gg~5vTyU^7XzVAH?vYoYXcJ=>hK18Cy9=lOF;CLL?A+S} z(~`d|9_sLD(>)Ud69+6&uC2vnm5)cc4Sz9fd4aS^?~NnKs%NL=lhUc`OA1*HKP(cRCnJO$b=C<3hAF482S%_0j0l(rWK^epHc{53%lS8F*)e1E z&Zxxc2zG!qzD>)c?3Q}$^fL^0X_v%|fMr}o`G*B?G)(HI_Y^6)rl_rQfZ#yxB}c9<9>hJglN~u3@=$aMVrUQ)&}y>> zM94iW0Rjzi=`K44Z;a@+C{Att+ti2O`Hqm)@{z6gn~4nvqBdTbKNafXm85B#%99=M z>HwSWiK)DM2Fr;4*nBm5)XcDXZOlmyw#KB%Q;Og2Y3!p~PxuyH0o3NtRhF$6*op!?0w7y?V_Z)qQ?20q}2SP`N zk#0EDMe+1Gu+=>KY1z0_E|l+1)$pcyzV+nJ_MK%r3Bsim4}GeV4Bq|J2Aw7fnRIr)4l~0TH0B19%>R|lI z>p=hX7k@Ku>uA4uW)q=}>ToaAM_*{h!ye_g;oYTb@z7QGO*Sp=b8$4IvA@2a&KMI=C zZ!1sRXS>&*0MMm+M<8`R^F1I^KonN;u8`8*Vx4ap<~=7KF2fNbKGsraFD4+Y4T#-7 z>$GAWS0uVL<&u*~^t%;3_4Z%CtApyE0n}E|J4tS`QW*EGJ<#aJQwLD0ax@~Bf~@w8 zXmQ5OF8Yw2@f&>cx4(M*vodkBQq!)c+TZ{4T@gMpOr3xE25Y?nxB-t{Sn=ho&CPca zh7Qyi_YDYv14ZBeWsjQh3G5gW)4E^9#b$|QlOjBg`A>ec{N|tkDz3P}#OT}SFl;2px%k~~klZEdw6391e;SQ1D{wg z-k_AR3&h|5`ci9o{t<&fLJ7nfC+0 z_QgfR1I;9vQF5-^3Q%H%YW!Day)dJG%yH%?Q#~Z*L;FSBo-OwLwfPb~T#un!u4;QQ?_YdYn)wVm$bicLQ&qSNgdrJk_|od_Elxh=Zzk z4mLqgF%~WMV(RE!1LQ@9xXAqiMq0Lze0B7b*1Q;lms$8!m%bFbA4KD*6kciX(?TVY z{)xO4&#t~VmDE#mB<)T+SK738w~LJSmTGptompHcy#MrmXYW7kPr`;fo^$T~{$ITf zK@}z2o^{fC2KH8=$y(LkftKADCvX2KRntLpvxgGL`t3+P|L?299GAOi<~Z`6ZWwXA zSSlx_>bMtwme=9ra}xG1zpvVHR0nq2WnU=$`nONOjWF1;Vd9p&QMIWf(%=NmWXR{A zeX=NSG_r-pI-Ye=q2}7Y-ic2Qwo+rtPd{bmh4h7<|}BPJz%RVo!MQH@2k zB3OSd{G8ER=lf`~a^?1ALK2s^aR9G1N1sq0p;4~Ie2bLxab4@kA%}yTI$`sJ_jM2- z4A=*ynqD-O)okJ7ijs1<#poik(5rhfJ?zGVNa&_fV;C=nZoA)_i5<6{hZ5R&WC+b2 z*(N_;$Mpz4_Fg}sDL~Fqkd#o~?vMLffzo;7-6gflgNw=nvsnWWJ%9if`0=DlLTe#0 z!wo+$zeW5kLB4zv$Db%5FwbNwgl;ULAK2IrLIAq;?#s77cZRj>hIS@=Erna3s4Qs3#9^8eRf+u&Iap_Tz1DU-R5lWZut zRwDHg0Z$2_N!%{Ios>f$HoE^l>wI2pv;_Z6WUWwS5Qc*}?xWGeNAFv$qghUTA1;1< z+Ns+WteEci6qWg?)I(07GPq`pQVe8slp$xsX-iv6(&$&h83V-FKuZ?r z&9w0lMf5VvbH`b1NF*UB6(wYC-oKYYVVnb>Rz!KL!?bB_j=x1Z?B)fzl zyc5p`#j?nv@(CYE&yg`4+@y}9^1{o!S3#-I);$7Qmf3!Kk9=x$zo$0Fo`o^}8DWQz zo!>HhRA=1{v7*=W@8Zw>>L@YPP|5}_XpW|vkPxbh+=^2Ax`3T z$}fIWzb@I*@nyi*D=^%f=Jn%fQ|R{zjgHMJ`vQlqvz|dgnU>3M*`$GvP{~8q4IS-H zJSv%L0#R({^8Z%lLcXq?e$XY}asFts(S*NCvjl)Y_hV9{3@Ql}`ol`GUUrWAX|shD zT*|6bDi>o zBZ5)9-SCJ7nnc*eNnV6j^Q3?y!}rG&faCByPZD<`Pj2FK|4=f4APOtTFMUimd2{7m zk=Y1a>FQ_vKwPvdnA`PcLqcW}hD`U!RKj>hS5HP-Y3Y+*$~TK#AOD5)f78!aF&?SF zASo$Gk<8k-yGMi7s~pf8%MF5~ZRGaM7Fr_ZzT4cy=rrw%SJr!ugnf;LkxSGhK)DCl zEr~^~u+{UUd)HFZ)VK&+atPlf=;*1YNpcGvl$jXtnby4OH-$xS~v_WOm51ZK#}g7(ijs(<7|0x# zFN9xT0-xreaB|iXX>=|2x$Am*Nhd0&9mxz?!j#ARgRlwwaaqR;^qX;$(*zgYX3`MJ zq?Wp`#<%{^-f`M9_W==Yy<0NGZlxC&>L>mG@!QBUOJW`#*}SfqZS+vxMg8fV+q2?z zsF2GEH(2-0{MtrcPEL;_F0A{a42w@`y&aZ0c%qJQ_2DCn67XJ(%FAjHpe8Bh_&)<>twrWiHwjcn9kYE2Eq+AfAya`2a7nONX3XM>!SVgBaZx1K z+Md0yZBSOl;Wf7R?Si$p8PKxKEv?b%T131>#!rpfmXvn%Odj&UVzI;qUP$tC@AT8) z2?5d_Rs>Ld5*T2IH4*j->>)6gvHY^x~9(+({bRxEeWTC zi)L+DPP}_sR-EtY%Cr?R9XOJg&Y#S4|6Uqca7W^;HLS3fVn@R}7H^ojR1xo4{BXQP z2woH10w+PCJZktsWC>T)gnu&qo?mLW6~#Y(RRu0xm?6K`PG2o}z8_i)vu=0VHWBX* zd-biVsgVGrL`|pqEozw(2gCNkg_>8@b10$y?Vld=&NHy;bIpolk_9so8bB1gXrK7Oki{=*)|y?kQWgm9yKP#;^f zNFMKi0u|Qv)g4Rjo&kSl!XKEr4X)(JcMBPo0ft)*2TP9wPfo0v&2m7(CEB>^<+N(< zwUf477w$dl+kL3JO2Kj*k|T1AWM>TO-41Fb-oBzmUing$sOGlSNFXvOuiJDSiBhF_ z+tV+fp5v8b&G@lT8gOvxDKX4ACG@GL_UYDdIO73^!$>R*GhR|O{%oi#ij`DVv&KO6 zdM{C%HPbaVK1le36r~;>V`!WPt(HiJONmm=o}v;fF*|^meBJH3mIAL% z*|TH=>M6o69&2>6Xm!i+g$tQj`RGU)_lJgLdCjblmjP?-E^XfeLbjpuc+fKNZer8H z?bwxevuAlnC=L=aF2e>GwO546hx9BQ08CyZ24a$D+jn&}_{+DN^! z_Pl}O?^gl-LY;KT=-AnUB!H_y&INO!1Yi{Ux+D#;#|2x!hpn=<1Vul2C%^EY>!6g5 z&C~M%#zudFyl))6zdH*+%(JH&PI!7Pn-Dv>#Mv065jlSC&40T74t)yLFu-JHhJ#PL zdyMLjv3%BO7Lh=(6Rm3ss-M;xj%Dt1oLK6yC9@9gvSl%V(*|?Y5E*##*rni2qwo?* z$td5KGe)x_bn^}H_DKNmuN*e*ijKa_ZZ@>YOO#f!>4woI#dS6TW- z&LT94K^toZ@O*l9(o?n9rSVZ3&~G|U&Z_qshRK$?g| zT0vno8d0PnKSuD|oIaIlfSTYG1_w*qHSAP^y=RG@<7i-uJP(=?+mp5)y$GXih*5x^ zo$SM=7h-3a9pu1)cItc>B%9?(Jw`7Pwi~nJo>-P~(|o3cNk)E+3_giVB}fWbX8SGZ z>?uIASXzq5^<4sTD-E2fb{`c>;Tr8n0)5++LE0I$r3Xr8cZ(2Q0P)z9Z?kv>GnJ#! zB+`J~Gh*dEFghlX*9F|Ic^=FJfi;+#siaO6UtF$)3;l6dZ~OTt zj&Oe)s{xe)Ejaw>i_WlL{?9-CKagA)1VQ)i)zga^3;iY6lUROh>`a&G9>ngD?@LVu zp;X9OrtYH0OP+IF>*Ww2dmr>HMu=yb!xf1lNe>fcDJqa}EWg~+<Bcn{x=*N-;?P>Cpq$%!p6udk52a8W6HCeLaasRVS4)Tu=QhdHz=3`ML#CM zTOZBc23!!(X(b{3Bo0n@(%rt{ghuR8+Z#~+YpY?@+5othLC$F+`eGyS8U>;>KB5M! zAnr#=W%gOj#z+qMkR`&#d@UzA|Jsy+44y2~j0x3Q268Wdj%?mZl^^(~_}u7N49_lu zUA5;C#kC>UdPo~%>thallL}+PC5=l6T`9VAZKMMnZyDnie(<QzGR@geQq>BSaUEx7>R=IK5~qHWkX$bnEwj(-hx-cv0U6rf>qxZ zovBU-ZSj16dvm+Us*F^)J307(tPNPB;ImP<_?Ya3UGVyyuwS3!&XYbZ-lGS$T3rk%R{9enwRJAz6S$iN+O2;tQG znh58|#vEHs7tAW|z~6n>$fF_%XptIcSV7;$Xii8LfePwio!{NmO`r>d6dmx`RdOiwKdXEwqlRt zx9lk_n1Ss0=xH|nm!FM>10F-Y12t^rqkoaS^-4L<*KwfDy}TW-!XjFwiUD#+wg@{8 z&(jS(06Rd$zp>+awSFRaD3CG#pG5!wKmbWZK~y-`Y|vIXg2dNvSO2uv-(0OKTu0XD zpItMKTddcisQCl=f1eI|AnggqMBiriUqOwBfH0EcqtLU?M-wbNF*!t!v$DHy>*E^soQ@$)E3k@*n987X7O{lmE7S zmSKJXdLB|lf2Lp6Zsj6iS5SamcNO!G4Azl?+`E zT!kQ2?UE1PMCz>7s(qbNKbMSCmZnx@y)4!TObG9Wa-6RfWkgN=k&8b$*4dM*r~ef2 zoMT|Q0~ZvIAEDRVBX2>JnPZ+Fue)KpySVuAnT1V@g06D+)L!DPq`m?UyB*+k1_rN; zFQ0my2;svI(!a|ejca;+4krZTMAW?pzAuuu--zmT^ISw>)~+)~s^^bD*|kA7y8QIZ zxvAzK-G(oZ8yfY7^)arLYA`If{SX7twU%T&k_2V;fJsC5d^Mkr9ZG*vZZ9^5CD{9s zU-$COPB1V(Izg4hJ#fC`&cEonE&;Ly4y7OUKWx@*OvsgTD=XlUm9^Ys$6r!tK8%5Oe#YyO5j{DuRJO{@^ z5)J^MJ!m$md==sQk>DQz2Zcx%lNA8Lj#dEEw=Mrnkc{iDZ(M!7Xty#?fKp{mhm3TX{nQxfkx(tvEPKBzx<;8p32sGJ!Pawr`yT>3pKmd#Oi4H za47PW#>EzGlblHExZ)psw@Z$JY320mMJLpvt!a|>7UlBH1`lUp znAV@ZdJ^_QBB!>rw|tFTgKd^wGEP&(GTf2+M;;-qmI833t0HN+?Ylqzd44WBj&*kZ z{&zgg>TEUy<(70O@{K0{=bcfFh21C-m3Clnc7XB7p}X<=FrUy{e%q$H6y|ExKtL6w z0wgP797@!D9>k7+>a1^F5uzcM8x7*XlBkF#+na~}(gldDJ3_Ul?aDF&`WYQkXlK%} ze#mughKjDq3yr*#rv3B&)3rS6TVX92gBe;ciXOGRDc-z2v1tE)?(sE!ram zKJ`esVI{-LNN*!%It=^U-?WXSJ6Y6D_Yj|J^~hERXUPV>BB9>_JBRmMIFqE!9=Db@ z6#D&vD**i%GCN1$gQp1Erg=2r5rFtyZ!V^}PS#3R+acrYANLMomrj_OH=Gv;q4Y#h z0(T3Sk%y_Z01qg!r`4{;)2C!E7rt6P)mIP2SukJ!{(C|%fXQDIyWCSEUooTktU565 zdA$TVjqN;w0nN0#5wl6NM|pcoNmp4F>M0k!Hf8bItmwyS_AWac53lj-x-TyE@oEy?%lZ^0&8ij zT%L4MXb534Luu8qjKK||@?I)n!aNZ;>zOQ)Kw~*Dimed?oS&gs5=T@?MUT?2Wo3zvRXng~0yK}V=r{*l#QxW`aG5#Jh)v9Z7E<&Ud^TeuDTHQkVT zv4^yp>n3rNk=3jqX|1{${s~4&U32vHpu5+bM*78H#RY2;eldg|5^hb7&8(Apd3ZNDN(PMmlyfe*=4F(!!$82EzA8Q{#c6 zD#JW)Nj%Wlr~~~yqHU2U56{+rTNzQ6Xooaf`;PIIXj|flKyAqX2>whYj{CeNgJHR` zu5x2QKZU9pyF6dAnKO9GJlvqu#HC`CKy@&;?(5!gzVKLZDPfy(m{faHNd(k)>7{PAEWP`^b1FBs$Gl)N7 zoa<)4(#v;bIQ6f^ce{W(m`oj>q7zRvY6jj-)__eJ-xlVpkF`G>hnvwWEB;5gCi+6?8kDceXvzm(mpqx!jeTu~slAl<=O*2af zea1B4B4UDo?$v2MQdS>1dY{^5GWQD6@v~Yz-`{k`8(Xd1KkWmV7NjRd3 zOejrZr3=DQEf|#tDJ2IBKoV!NqN^B_n-m-o4Ie;HDWE8dwHPIJ%JF2beT9&rWpP!} zW!tHp$6X+7$B>@4g~CRKD$@i!2&=*Bq*xRWJ&Furp1#L9(>n<6*x>bV$)K$UMHnyF z1aO;>ZeFnO6Mpo)W@+aPrNGAlINj^Mzz3&$!9$mJC5y>0DHtEnzu^9Kziz%tMsLDr zd$?|g>dm%_G9sO?*qxg9v-@q>NT2CcBDr|tVQza&4Tb*6ar^kESa3hLN4_pg*C_CR#8{dIPU#7?{?+nPwohrR2bppLxr;QJCC58G^&0*ZX^uQuP zM3NFT7icqsMklOTrgbRuJR{IP#fHXMrjnjQAVPXYC>mB5q5Hx=ho3!)Z$bcL{vj6& zHL%L|)j;3m45o7sW0_R1XB3U^jN#YR@nOJI6610^xL+Eg{29tilkZSYV&v-Nxxe;I z5yN2955F{!`q*optpci2+Xwhs@?+Sx0m6g69JL4cbZLvW64A|O>!<(iWZxZeGgnwI zDI~*8?xR^WcMfmD{AX+Y?IX3|ppq@YHx`=PlrBsxnc@M-SGYw3PG4Uv-uYC9ymIJ;t5wvpn3E z7UUYE{!?mlc%iIJt%RLm$7qdx#@g1Rm+qByvz*s`EaE_bVwko2!T~&s#RElht!f{bR%JcK@-tB!3jkHct>DtE} zk4hj_W-oYjDoG0goTnOG0Xd9l{qy$&VeDny@KKCzmef`P&iO>$&dH4vNaR_ZEFB0V z%e!hg%eD1R49pE2SNVU;rIRDpkFlaAB$WU+Y-HadYXp{{+cQzh$c<$jcy=(#=`mEf zVr%AV#q=8lID$f|or!L_FJ6%ho6R1$3kw`r+1%DF>0}Q$RBN_6>omPlm${p&Jis+s z(9LT(-;pMFb@6GN4;S*pET!R6KIgO?*W)(pBZ`omE)si1`oqkKp*NVB4g?6pfJ!X{ zeA~m=RobEo^e3~Yys}+u1fPTRMyUU@mW?*W;V6 zRD8eb{}BQFeED7C?kPqdzmd$9b1Bn%s>59SqU2ZwLr4t*swS0_4PP|Q(;{Ll`-56= zDSg~Z1`r)rO;(X1!0t=wrt|qQu$&_*o)^~nEcu?n@xB9<+f$}YbXoV~STaZ4i7T?X zZGLsJV7Z16L7SAC&QCVAy_??ayI0!QKd?+ZuSBiJa~5d?^Xwu2Og3XiGy38Zo1Jje z!|Sh%QvyAGaT_L3;wJ*D^y-YTYtlAzP~rGXy^R#VGd_z`>A({2Z$XVIykxkH1?gZ9 zVV1P{6P9MOkNT*+8DXF8`>~|2OmFjK9ecBWr4idQ3_asXSoMoB$5r^G6v(af;D=3+ zSZ|dz#RcZW2bh?kJC`SahByKfk~I_gDo!``u|P=F>0EG_vB`Hze1uY-IJ8y~ks8a_ z6gJt9=EH~8Q0r>YF#6Tc3|XHJvOnE#=}rOLsgx?fwvBwu>69mpGBsm?HR0P}u`Hw1A@gacN&h;(J-{H#_5w=K$^EIIr9)%Sv&sN=$TO3h~8Qq-L? zMX0o^4f5ns?^OmH64SWffHBz*9VseLr)^Ktn?&nukhGmPe2&h4g9N4#sS!8g(dw?5 z?1+A&kxnZ;D$I;`+aGtde7t!USkHTF^3}2Mqp;Jr+u`6tKb$N(qS<#V7uxJ6U^@#U z_H1!h(My5=s9e&gKrg&E@wTiVG{MORJ?iJr;frx+rA0nkG+OGU8F-l4n=haCC6Wpv z^fHrH)N3vB+X-9=8h@H!uc^=r1Q>NL)2x#6CU51@{%E3##_I4gW99+4(#q4*e!9g? z!?>J;8x)KVsX!v55?RkFWBD*5Rj_8yWTA%R7wZ=OhNbRE#Ev%+W`?Q_n8EhU>EA9{ zrZ*%pSp876pl;k`UV6M>Euc}2a&;p+PZ+MBT1y6n9N#A0V|+j-P8y;;9ORsu)dVHI zo<%%-S4_ zHCOj9sx=BUXEZrESE^$FsO-Az_D^@u3HW9}$y^yOUSwNRm}==WxNc*0cBTKZ8j1rV zoZ3$K^!jS_^FPE?$+PnFhX-@20)0pxp~hH}|GMaQ6*#6}&&wzCA}O=^<@uN>xHG-n zOKbw=pb5qnljg6tcR~eWg^Mg;tE|8bJNm`H+kW?f*I2lh3Pu#bM}WB0TVBLLokV8VfdS3z_s!h_qib-WHtXB;DTkx6W+&Db-rEAIs zbppt;LYFQpD{_|Z{FU|yUIR|zo+lDa6I4xo=kR(;*b$hd{pj}<>TA5S-dlsehCd*N zmUar{cUGAQLHHCMri9fn)5^P{0HJ+=qINgUl5X!ol+4ZTPG?O)2+NMkFIRcT>+0MA zFgVEQ{^;sS&3tssd^zLO#X`1Fg|`p%bjTAj#KG>`)0o$D3MR{7xD4Slxjq73jw~%a z7tXnDH`dmBdU|m)5*-f3eU9y?vNq2%Y6Y`qX9~eO0hTWsp8v_?J@?Ke%!&z9bL_50 zuy0HbA`VQh%<r8F)g==*oF3X!C#M@_>Yl%t*;`oa|dKiS3&2@ z#kXJ0|LzIki*tA)lPj*-$0RmMQC6=myMJ8P8%2M^uZ+r#P5f66&aQ=EMi7!|%+^Ol1}!Z`g3jm*o>Xbjz@wX$UK6 zfExxR++dvap9bC|KU6Lp!W#wm(ezDUuG!Z?hec#g*ya%bWGRn!kbxOTEARKlZ0q5O zCa>VSTNXE(Wj8s5`Z6}$Cq%bxH&x_i)n1K$cgi}=7y(3`d&zDkOi?E6ilF9Z+e@En zAQEe7Ly2vAuw%6W9An5ft(xA1P&HkRL@N^zUSyWFU~fA1@5z?dPVmZ)h{`-GC#AsC z(^OW;xR0gdonw&x=lEw{e)nBi4eH8?thaw+RC_N4$jv~TT{y9H!lbwZE3j=n3JS3> z?YUn-ifP>8_L1;)!J@?Y?r}%sntBeOnYdF9~G>act<)yFnf<1yWCx}>S>iA{8odw-?>-)Q&w9e z;A3wvne_oRUT17~HvX;;(F^aeF5SY* z6?fh2KQ4E!(@Dpd`mXx4VcaAiz9F@Eyywc{9nHeWTRLa@WqS1zo;#vxZMhcOG#9rV zQAPO@_b*}!v5-}lCJDcaR)|WChLkTU?u=qb0JX_zX_8E*eDk{1xw#c5b9R4#ri{3kR@9T8#!k1{P2`0}Hx?orgHZW+!%gK!f*~@M)JZ5G*)E4q zpjiyPsb4@-OBX-!Y%hU?q-M3#fLHa4Rm?u?1>p z7s6&^jjo;-`LOSfntV@@;}AU}(=g#u zu6~601gOLnBt2LNbpCUW^uUKYW}@1gd)|BYShx)W>VPQ77%_OC?(VyxDDaaK^`|r{ zuThM8I4?vMeR6xlP|0Z0rF)7xwuM_>81m<5n~)E+V0GDpWR@GYYg*8+L76hxOlMo; zOD;?m_%1>OOuK2~wbp-_YuY_a96iR)2SNK#KotQZviNr@K|f#}9F_lq|DvSyV#s@+ zV`Gb@(TOfb7jnW5gEW6-&53@!9Pg@)=ZUhvhQXk<&W-uMOPA|8P)%8P4||PQf+)oD})@*e{yjh?uPvtA`fc%>lY8 z9eC680_C{kz0*w4j!BR`FaK+^}qZ|FDg75$kg_fWhv5K}E zLouegfAWW^pkIBKZLD*|)4jCeH*)&?xTN9QyI3}sQKs66GTE{UwrOnw?d~J|K?m(@ zGm}peoxm@<9zz%;KyJy(Y06%jJc3~-rd&G4F$wQ#J?zq<1p_Mb>BX+#(t!ATds4x~ zPFyMN{hXH;05;Mebs*2?<%Xj!Evt^Im?3=Xn(M7#CPSb!w5?Nd1R=RztWAUDHKfy4 z^4<(>PfHeCylA|y_@^a|C33#ssU%@1+sZRF zig{Jknu%eNiUUWJgeQk4du)i@?uQ-sBxQdxZK~<=xmvFL*QT#-z9(<3nAUVWzo%!V zn3Ax2zJ!>=wNY#z2)J`P1J)0`;06XIz)e;BawHqzM!mVB?|`3`%FmX-E7;^Q5Neo8>USSk&Gb&DV8`#;yhI zw-0$Bkl!w|3W%hR+$pE%-y8OQlWOdn2LJwKX#Dui_Ris?B!KwP&tIU}bh^Uf;n0KG z$U9XE?;DgOvfxfY<^Zkz6*tO_JO$2K`>Og(qh^-GUeQiRRpx}*Rpk_*TZBr`_L@Js zTdBvY>HoYGFPImarE1upeN8#1bQ@`d!cMbgjT7_r*Cb>zCAE=c<%tmGIrB4%PZxyRh=qEYL4{4= zn`){H>Np)Q*Mg6OegBc(jBm2Qe~_^jYl>G~-z|$IQz}jeVQDJMt=SK@b)66O z;nK+3QT~Ah)#>Ylo=TCAYmty7UR=1;=4la1SnuE&3p3=V2#+c!5cjtG4E&9>r9$8Y zh@%^iPY^(SNr|i5Z&9@SDJL%!T!g+H+Nvp3>#A`_Q)$2bK=>NSrzAzj?W8~tVW8O zPa;gg5;*nc46wA_3Pxvaks;L={7{E{YM%iTP?ZJD)G2 zg*^=(@JW+(1=~OP@GvclA-ZHGGDTqret?UA+dZ0xjkM3hN_dFnNq-pP=I!xkcVYxf zwL0Uk%{)M+q*mtvGLg9=eWvj`fB=OZjp_FxEJ=f4wMqM2f9dbk|-D;5UAD%w|;1tTUseqV<4Uwn0dG_Vzc4S3c#7_A!#LPRhqj(O=7RsU5$3 z)tW^=!6!Zi#qRSca8KCV~;%}i#EO7=M4w` zlTIT~*!4jd7KRuga8S+3p3D{T6p0a|8`QBTJ&bKi8oq1f+sn^q+VemDAQ)SQPg)JD z=lN8*$whWInD$sz#)LOz4m}eP6m6f8AK}O&!hXL`mJ@UKN0P568xr248%B&+G}X*_ z-sx#m4`8sYy=sNGOs154nHVk*#a!8YMe=H%{fU;Aqr`0)`?(a6^^VNGmjrk93$k`^c@|3mtkPe@vy~oyWc6;2G zmjpB>u5M>tQ0y5)u&!a1L1HaKXyO}{lMRxhpa#7bYz^nWISNM5o@tI zv{18j%7B_H!8Dlr$7JiJXZ068i^5pM#{>M2`JSeH>%p|_sD)K*33x``@7lR1QmLR= zOtT^dGcOyhu5K5m&0D87rp1Oo;ZS{m=_WMW${TQ)LIPT{6Pht|w#AP5-96r-#?g{P zT#7v?8>Yzg3Q7jqkM-Ht#^l{^)4^u;%vaORQn;040>c|(22CUAmSCPbUT?g_U8pV_ zr)^QVX_fxgsPX^2y#D@)=26=r?le$01){ba1T{%nhIN$0t-5f+?a?8pfnzOIjeBUd z;!rYIh+8YsvG#kP!K;*4KBZ&jBxgX-j^KkOgy$=)YYrKXi}+~q7x zQL3FcQd~_CqD=1?r~M`iHn6rDuDS$UB@XuM@qk)sHGv#2N31dfC7l$Rt)}v6!iYjv zb085QD*4|(ZSj07SU@`|nUGKwl$#G_aEx*{u|s(=WMD3{s_ERHu|QB7mpw@WHJ|d- z2KDCTTPQ5dHJxYjlRVZ61lY<^3#~IO$(*w{VMGixQQ)E?Bh^HUC@+qG<*M($dA`Ja zzanucV2D5p%R4QK-nwfF<L=aS5f z(NeRuspV%1FLpGfDx*#PrW`t2-&f30)XeN@zey_=f7^&lFErhOTCAC&tKe1~SpXSI z1@_^|tAkyO^H#r>_?p-w!rIbIrWWh;_xI9%K3&~wgErL$=jKON8d>`re_&)w_u}@O zYOl(U=6m*%T19izER5xGg9fp0=MaFS?GDpgm_z=o^j)DIO$o$F55kz~S?&F5l2yFB zGBri}pnt0T$bA{?pe4>!;Xak`T5Wj>IzuaZv&lkByjaLzPLvt)EVcyBc3GU~sT~^Q zxFCvG@5Z2Ky6oKAE}DHDHWqaXW(Lf}353pemng{Ll=E9fkuHkqU1HchFLoWW-GL8R z4X1gO?vg1#MkXLDb0(fb{h+qH@Dq-@3+9ISBM_4X&+1-c+cP=_X`=-yx|F4 z0nx`5h-JlkkO}p5KM93yTu~EQNOd(De`dS?=Z01E#-#z>i})S&c5t%7OFS5jHmLepwk(ST>*Y#W1zU4Ij@dl%vN6v#GK=+rs7tKx)=!}#x5$) z2X*9t8Vi;K#x>2TBPe}uqg&(+VD?;~fi^=Jch99Yb2?3#uD3n@W<8h|B-7pD#;eT& zU@2++;Xb9H3G`M6Md#{tHXcluEAP)R#@UCYf{qi46+;)1(7K+h%g*~s0V&4MS$qB3 z@BgoV-FIj5Bf_PVx?}JF!;Zttt{cy{=nCj52=wvT%Hi070CMt{8xTtaf_hq!-*z#) z2e|{|pI{@RJ9QPWhLa{~OD8B4$nF z8Hm|{x=PVhL3;aqbjUukE7RO7W+p*q#@qcTjOe%gQ<k)>VRN1#D_ExwgXEfvg zr7VI=p?ylV#-E)1zVpwd$BE*Y32;dO@8wS7K}382c{xAd z?Vd>YlUk08j@ZocQS~lTN?d1~CQDo5D6a@#_T%+V*zq8)+YEb3F8?|Uqi17qE)YvWDW0YvenzVr|Qi4lvQV(UMmQi(NC7QWyxjDWM zbs?KD0;z(welZc2-GMh+H=o zW0IR{*xsaPBd{$Efwj~)=ew{%$Q8*9O!y70&h$mwP%FclCEy%!TjR43XZO1T@#L$g70a~a%cCI#x$9xnRy%1-lWyvSB1vI=FzU|(L(oa;gHD?!O zL)`5^U}&^h_5oSJIESo2tSgQzIv)*!^Byt2VgL%+lj}#sKRaN)-V&m=tS?$wJb) zJ=bR!uRRq}Zm)QAC|O<=AKEj{+Ql@H(rD^J>QUq9s9yxaLo=p{O`7^J{IkR7go}B} z>RzM?$-9UD9)U(pKd!CL=z&AY@*Y5)I$=RXZC3}{JjR)WYJCr@R}Wfh8gk zNuulCZ1q6-bgc_#8{jk4YiR3Izmmjs5qG-hn*M$y>8MnTtj|swA%rR>5WfH(E_P2! zC8HbbTCC~{*8svU-GfOS8{oLbk4Ru4%u0x9Bic&QS8=o;CyFpu(7Td&bQvx}n)_0B zI_m?DDQ#_ArB<{&v;B_4Vnp+o)4xR!wi`dmM;d}P&6#@wLnCab8Jyx>*A0R6RCuqJ zrv1j=3pSLPaUT$HHz=~Uaw6~s9$Xswdhkq{CVj48ra&a{6|esxH;NI;G#)k!G)E-! z)GRy^)*0foilguj-$w5mG@v(7t;M)hlmP{PW!VOVS32RCk!h*yt%wCKKK9d7E?b%) z>~;Y~UvNivhxeJG72EYRi;y(DmL8(3ea%-cA`dfi9;$QKdr_xo+o5tTW?~&$1h2%-59g~I@pHVNS8XmI3N=Gv)7!vR7a2O(P1vNU|8qy`cWG@L1vWc@)yyg z_A*S5;=6vx)gKFoSdva>^{MblB0YUVorZ;<3CotchO*;@(;b3T2B1!(H?n-(NzcNd z`~ye4BIz!#rR!O{=l1shbM#)%cBSW?-pV=b98SgoZ~!*D$)-q&ni*?CjmuMIU%LEf z`pP#h-%Qz(WobAPNwL}JZe*OWb67dAYec${3e$PXGad9LxDr!wC z1VBg?{}&KFC3)5Le!(u2HkmFSD@nOxa&$B=KF39`F;H`id2(-j3R_`xH7xV#H>-L> zS2*)|L}M`w2MNaOjvw}(a7cUL2wk+&V#;hH73X{-X5`^(1MDR%KECA`c2AVM-ja9U zX>Aj6;L03ay@eUF*jil@e`4BDcibyQHyLG!YLkI1Pj)QIj5bz>Yt47pmpA|4Is^$U zxFlV9h|5fYU%?CAE8N-0TT(ROrO3uhFba}Oe6)%cQpw+*_=5U6XYZ|6G z=4@uSO0ySxq>QDEFSWGS&d=ugClYU`j$0)qig42g_KAf=OUa&U8mH9jq=dm5TuIBg z!j}>EbFP+9HVR2ocj@%6Ka`hS^2ZQZb(>=b+G8PGCzIKbc;jcVrw><$Ks{0hA~UIn z)>Nv7XFnxp>um>EqETp(l(9j~Mif+5e+QeCkYt7;gK#B8;>+6-+Oq@Z!GbZvc8;0t zZmz^#`bztr*#Uqe^`ZHzWZUS`0A|Gj(Md_VP-95n00idcHjQ*@)7~gZXP@B8#Fpsl z6q$^yH70+{+MAR7^LzRPlott3Yp#uO6ePdOB@=uj)B{Cujy2!J6ZEi`Ig!6ejFEAi zqeUJFOpV;_ri5*L(dBJAQ9pxOQ%oi2sh&=xL}&5R6~Hy@lz|-wYN@*zCr8Sxde1cL zqg&&^>PXDeN5$yui5H0rFYuh<8d<~`+q#|FT`Zpfi1kE~bfxt}#sSx6EZ6P)2MzQE zXd?_%ZN{WytG$PwFoi)HZ^uR6D|Qa!{gqSoe)-4Htdv}8uCu*b4LV{pj4aJ5)p(DR z9kcQg=df1Is{k`U_9hbCbm|-Z7xR8imCadu+GbL?ip5GX9_`uA;WB<}qqM4cZf1eOum}1)h@zc$D_|Igi`JE9vwfYq4;vN(s7C{ElxNBrE`{ zmeHtQf3wN*(V?J%Hue+A(Iw8spwWB>8%R%~1v#uo=}I#}AD@J(+P0PtbC=y#)VkE} zoAEZY>(Ei-9p_N$9E~e|hO&dWh$Z_`@I_($QtN~;O zVSp1{&w5{?+PaGE>IY0n@DZQmkruF&W8)?i#|Mi2yKVF*Qo)g0aj#5zNJSg~mcQ1L zb;cm})Qnl}EQr#85wNqi47mv?9(MXf|IAk4O)lEA!xw?o=7dF;e~^WyL|tYB(aEk9 zKAZ#1fzFi*o2R|;Y2P|skSaZ`LmLD;YpDFdg@9&^9X8wm%<5nFKvpK)-%HUUgjvRD zGA_G2b+g)yI+EYRI#I#wSB5r#z_U+#i31T1Mgvij7VUaOwaXV80I}WP{6%3cy}F6q!^B1$A@@ZcdC2Oy&J!Qiv#( z)FqtFG|FOz#tbJZSE@3a@3N84 zh+sHYG@}KO$=%p;7$OM@1=DKS(w1xp+^N{36xC8tn2Mo&?jEde!O>~}jM-F4Ln~a? zxeGUkM#wsI^p$G7VfD;Wr2bIZ^J64*Lzd31vZ(=X1v6tMHS4x-MrNQq`=3_ej4)TB%m>d#m^&iK18 zj9x)ypNDjvUw$3!#x;`*%Hcp$rZ+qJic+Jv6F6$jo9-FlNeq0PFAW_#IiasJxuSrt zDQC%GtB^-(xniS!q?8J`L$k{!C-*6c5xPj`Ao!E;?$Z1c3Y#|{bF@~4cyis+hB30v zBk|is{nIfy2RudBap)a|zE}RQ24cv!7aMd;Lr=;Y!%5 z9`8O;YM-lv1M?kQw64;KT+szpcU-61KJCU@XVk8KTH<|8Xbj zE~OW4y%VuN_2FHiY~hp5l2?{*-BS}u8sC_iNPeGKXhGHCtDAL~tMtV+lgkdZ;^%kkmvo6D#} zo{DKy&$WRm^%t7Pqp7;~u%`rAjwnf~yDj@tYt44EAi>G3P}aAcrPY->k>fhTse zY_&`B?9MEPtxRZuKY(i@oM$S?&kv84(CK;)ZK+dN+h$dPrBsVlQnP)bz~5#F09=|( zEZ@#01ko;HN!r8UrwJ)51)6X59IDKc-OFPHW-S zI-G3ABAaw;rb@p<(aq^PPn~vG07)pqzCkJ#^QT!W8;!xb9Cu;LPV}?AA%H0{0n;hv zgrq0~a`Z)?;un!Sa>ax>`pWVsT*%>5a%9X>&%LTUNOEL7m+mvy@v^Qrx!WHe`cKW3 z;klCW25X}1yjT_B!)5J)V9B}sV=n*AFg7}DpaJw!t{l~}6dE%=t%^`4Pp?@V!pH7` zMu30Dm%P)H`TscZ>9{s(-uisS3l0hkB#4Duw)80aU@ zuEM8P=)(@~ecID(KtA*gGOSH2m6f(?K;DPwaVEe4`Dnd z+#cDEW!EM)^ll|@P50Q;;&>X?CxvS6aatYUW+@f1+MZ5g9=?gxH`HM}Rx;wWg#(eR zjZfn8j*+aE_QLaC_vrU*eZz&4u`Gge3i)jK6*zJzWbnkQ;^o-$&Fl}P6;X3B_;eIe zd%yvx>P$u|`jlZ9Pe=J?zd}p4fjsEEXBe3(ZhXbzxK0Y>PDN2?A{`V?pF{P1>Z+TQ zCr)^?M;6l2J1i-7Qj8n_;{7AIrd2D`SH6GZ*xp@;Sh|i+Ye}*%2OqO;i*=Xk{iX3Q zhl%`ptl=5P;Sx=4 z)o8$@@NRwxeQq~*+7Ga+n{a^Zf_osiQ=Y!MBd&P>)K|O)kurjcEp2N9)ww#TkZ=)# ztCaE(z8;|s8hdC-dkzAAJ`=`2g6`9G#!*durYOdv)!;o7Tuw}s4O!*~tG2}-3&)tb{F zp$u1t)hf0qTu^#8;9O34pvkxAYYN6n3t&O%_WJDvxcwd(H@T*UmdN2iRn3}*cDnu? zD+66iu6uv_bEwqEp>(uA7HV6e%V_5%xVBT};VLz){`5kNNg>FrN~+}6Vn}q#IW{CEIM3uZ9@8=Pll@{y)!R^QUwXG< zNt{rr!FYFhq&?@FIthRvXX;eUzJ0O%(@KNX%mIi54;`dxXK}_(bkZ%1e#x*;;VfQk zm8&l!O8{`eQFI_E|B^ZdT_Y?}2R=pBOaB8$^fYeRM$+x2Q|h}D^lvOU3D8`W!3HG; zF6z&o3&?XqKWdXjy;>Pb5r>-~-|;%=B*p?XI{R;28|lH9 z3RC7~u^cLFXJ8NN;Zw*K4Yps{%Z;69Nwu=K8c!#77=eZNLy)UsCAjc3j-BMq5&gl2D!73ZzI_;V#1;qoJwdb#B|)#bU%){&g+Rl>p=V`;y_x>q0&DJv#ZXxpn{}cRc0z6h+B;)_>e|^r6FRkV6ZiDHU)aMCQjo5d+V0B)=Blq+$ z&RZ+nXY4rpV@(-P)VD{^b6SQu-Bf_E*_HwEBB>ats8xpe#`tN*xWWh?k9)CD?ohWf z7cr|Yu25a&EGU&D~IQ626>q^C&qzHq3Rjy*fkd$N|-E*Lc__-g9vIN!ZdY$8PGZ1to-ZWKF4P-< za!EIDe`N6*8Q1p@;+>fe&PFNE3tM;MFr^=>g$ry{P(K{;uzgS%ls4q>AyUZdW}^>^s36Dy@b%O>muY670JDgFct&u# zI~ku#pu-%`I1~E2TG;jc@WC`=I*Gl=i1jZdoN>d@b0WUXfu_PatV57XDj1Y}ffB&8y+$`n+f(4XI%{ui8SY#OQ{ zswV%-|MlpKYqgmq>{r2PnZw|SjG*YzWOULPez{?Pm6ZM2M%(V(ONriJ;z=CSz$EEt zzoamR2Cx`RRpIS`GW;sz-6f@%l<%2Y6FE6QpaOE ztbiWyCr(XXw=AX_!+x_h9qs8%T}lODdIayn%$HTi0aRXxL+N zIjC}5bq<;E(qBs?=w4j>zWK9uszz{Fr4G4dhbqrfJ(3#x+xc+tL&CO#wG?xAvTiVa zVHvpl`_FUZ{J6eTvnDv6(o^SY?&`ebmSDg3bfkpG!R&9rD z6KhH`Vu~D=DC{uyVejMz>-_b*mu`Rv=^3UYjv%xP|HDt>Maq47ln9ZB* zxW`|%MPyPd#icp=5VM*uS>Xt5!SD?opoF$Y6aGH->CLw+jntO67njHAjrjAcrJe+$ zIHIiA1k=xh+4kc?J&s?!>GH{OA1mr_f2a3SsJ%qrQ%ai#KZi=c3f~Tf(??bP>KpbS z0jwTkvLrsucvmKzh%?WPy!nkgeB3a()0D0BO1kRZOs`CXnJYuQa!-}V8%yz?shC+P zn>F`w;_K7bH)9rz&7m&^KSXp-cb%HXK^%oOZ%B}>!q2`Uc}j~|3v1hUJJ>w&F^PI7 zqyi3lyIAKsZ^Qz+2Dc zdj_$DB8YEON;RFeAuRsy{~ax@mXfa7AGxwx(*TW>R>^+&I6Na8L3lO=twF;BUrBe- zx-cKPjH030%2~VzWuJ3oDSRc(Q`YdZOHdc0Kfh8=DX?3oJG|sVZ^8@owz05=DGh%X z+ZU_1i!EtUx~gyw$mlV=<1;8EKY3=`S+ijbTq;_~)9S>`t%x2U$t3DOPg8_G2it?h z!RVa_j#zmV6_02u6}L{c%dh(-OU6`w-X`@>oe7U(=bS;!D$)n#ngxWA9^<}3t&Y}f zHT=m0iQD3+<1SEU%)*LN=w;s@sdj4pU(EjTZw~>-V@}Muv6GnIb1)XvI~#!Qw3dT6 z)zHM=r$Flz$NP5v8G6GdvrakY!WhVB1?wt}K6-uMV~D7#`F+iq6j+({t_}T^G+!y2 z<))9n^2ze#LEIe{JN$Vmo01JjWEOZuzf~8VEPnfY!WD$Hye^IQsMvLjdxbHOBGRMe zQ8+ak;;JXw#B%3bMD4N!=I6*yW{yQ&ZPMHANVxv*M+b;QwW*xp-~+IsJPBY_MBVk| z9{^LUH4Qln_}_4OK!71eUzVNX_{Ulq9F`$9E*MJ##+2mr;NXfAs+%1lJl&}{Lc1i_ z_;=v@AP~z)I4-*|9vG+NFM-sg`|Vx+Xh)g$fE^(5^by$flTjQS*okY)gAn{yE$zE` z9R5>(?+nF1Av~83>+>52%dJ)PAOv+58%B7;1ff+oHxD)=lAIyAZGFcAvV^_DWM8hf zJ)X6#&2H>cc`uXrp~lk2T4#25pj(oL)*wvfC=L?d*_QiASg_vYkTJ<+7i^YPWyg@S z(r0Jg-vy9|1Th20MwbDGQ|%1$v&rYDkZi{KB9`;((WV~Wdztc{3@Nq;hx|^G-hEtK zXK{?l?Kq@|?`Kh`sqr-`we{AlNPO$H!v1**v$$`%QE-D?uoC3a&az{xxV+v4}ZL3gU$RpQ*>^TR~qcV8Mym|1mo4W)BB1R4AyF; zy06(pzr`SH^jAl)o$~qU>OKSr5rq)BQa^5fO({> zsD}7{A`wSH51uN`ne>Jnge8H-!EtV=Z|3gLz zVkH_PNUahX0Wz$0YO~F10LUOc7Vg*|nK#68ozX>W?eIk8EfHPVES}R%{ao17~0_t)icbuL?^wsg-r}+STi~QgWROPv~ z`5kwEc_QVTV&gpj_Q0Z9XonK#_b%`yVJi_=v%FW%Cjwq}KIuUzRJCoP^lzp6kfunN zxNK8uut41+pMMsD!IiePwo7hoO*tql;!p>`Uk``hEV6EHA9ZRMlET@q>EO*mF%Z;b zb#@|{r$M`0oU6h*93W}Ul)gzgs7aS1T2FvpMQ^;3qV%?1BqVFg#=OqMKxzs~XxYwF z9MXwC#lecKqS&XPg~o*hID2VM=$;Yn$`OL`z6?B(VE0x+V@Jdk3ONO+(fvqX#h>EG49tYDM$jw2)|QlvLxjwWrHck9QCDkiR#l z+d~5&M`4yc8H9mz_q*;3s;Z}aXxW*zW*ycALnLyJcKtTuszD`t-8V2E0bkuXvRWIV z!47IHrH$B|T!6o0%R&r*W&(CBV%-78`b}_%)4$j#G_|K=Uo-3r}$u7U;`;~8rQBJK?Jk27h%*`DR zqWU0EePBb@%#xKW_G9wZ1*fvVJR4t(WaqNU5{%oJ7Zi8LNlbG<8Fk$!W<~O+K)5_p z25H~eGy>?iGwt$BJ0*Hc&(Yc?Ne6{fG|~^DhY$HTqK<#Bfb#7NGATw(EFn` z4eTqc=4NI+DXNm=$osJrhFwpka~?)gku(0Dyy;{*x2@S?qR+Fx`b|?lB!;h z2d(V>2zT8#9$Heq9GO%3(XFIT-(e9~(=O=0`dqF9 z3#BCpT=u1i5k4Ib`c^Ml9$*3#=bz)+ho!!k4ag6nXk9uVn{rb@3>50l2|+bNs+7rR zkTZ34PfK!8Vx1NEx8Fyo&}0gF<1_j5nwRcPf^tIk&Lm?y1Fpn7D@UBN!>-hUTeYhG zvws$041g8t_A`j8mFzNIn0(ESQ|rT^!#a=I{O9V*{qm=s7k|L{;zpFwy;Oif@^u>E7 ztm0-E0yuOh?Yqwm7xjTSB5tI;3Wi&nuQ$Sbv{|xX&%7eS==b?C2IcrbVsQLAj(7`B zoHWHmk@0Q6#Bjf~2mxyRFo(t9+`o=K0xgGs8h9aAM=|$aJOxV$Mr6my-$1#w)ou}A zzuy(-ud-)1!AG;GunkI74i@ZCdod^!^(pD+?N&VJ4k+0V>htYmnGHocY!?A+h+j^^ zHa=!I!X~H1CfG2Br~+xp_=uOAle zrBT~A?-IS|_VkAq*s5qS;vv7(sl#9|459aHM$U69mvUx7vPg^rm+s3KqgARcwU@(# zp50WC2H6u4EjgQtA9zbBE+)rLkZdb`632O<?6 zL8d2{2Sk<`9-%Y-7Wp7{%0wRB-XBZzOYACRLe)KZNvsOaw-dSBYDAlR_@Y5v(J02% z31LdfIJUep-vOa&HweTY2j0OOGnC$?7T#0R-K3<5RlQqx}};i-JQ>azY7ifQ6?Sh3h5}7 zB<>7_G%^n{@;b8&NWRw1&Dnozz5a5%FINh8CC#6j&1S3Yx3g5-CkIfIFesANjyp$k zHuhNAogTQvG}Wj3ow+wFFGgR)jOyy@1A-M9eNfK&!t{QYX8sDphzah>Pva*qbDOB& z#Y3@h&KK&-eDo2WrGCAz9H&Jh90l&ecjhse=qvDZZWnS&~ zrH>*$``3%GGo%K`44J}!ykp}$N$qrlrj(;!&75vCOz?)C^gPj-2ULLvO$TNNpZfk$_*0Zfl!si-NWZB+UKfx)HcTv_yr%t+FAGwlmlB5VoY#o_bM-Lgx$FWSeYUdRO8j4AroE zZ04o8+nHL|+Lq&7$Pwb~>xSrrBnC-NAgwTCa{^Lp4e+qR#n2kspTubW%!LF93D#&rdoAAw)clQUU5JqkD zg40qVMi>4Yt{yE1NH}PMJMH$DF<=skk6t!`LmVtml7^)*BMwwXGiS3g4}o8AI(uIm)cb|6i9lf~0~PHgoE zOOaNiNSGT$)uS^A-gm8gB!e&Rbw5DHrSq!{tB(c4j&u;Ig}8SdCO>-n@n1(USvF!c z@*AN)7@tfU`%E4nPY8)l!66FBfO(Zyz8w7O_)$8u+aC$n5dJLaTenS*vw5N{`mVJh zUh?8G>n1fB7d`G-Pk81How?>nCQbyXrI_nSiXFfZA}pKrO4l6Jd+MPYFv9vvc=WM( zHs}SbdV+2_Z1#+wO7d>a;}gCcupI}{H{Wb~WfH`P7PXpt`wx5Vc^r+&sh*2p`YI`M zk#cp=xv!k$QzO0_@4~|?3HNn+6$lDsiaE=)*IcW1Qds!91nh)<@Eb+$}P&>CJQ{XPb_xkz( z=3$bq`hrD|_T(OPDJoyyBnycI(OZHj^k$fx8*1KvTbVEYQ4`Ot`@`61k%TcU(qFY_ z{gFmcm)Hx64?U=5^TID0EmfN-Hy)G9ute8yA91~>M}_uJg77K-H89arNd3SXpPz*o z_c)-?gO`e)lwlGb|i_<-sdEH`B#FA5O#Lo*ZDb zADMj_z9UIz5zd8}+Gw49&vLa-@t`KrrPqHSNd)B!iYV)ugOWr;XfBOXKjVh#U zH@j2KnU%V+%EYqG2rzIFZKfypoC$rto{#GEDG>4gMb`iAcSY$1j^WzjNLnFXo#!e|SvE zeKA_Ht1#w%mSs`&n5_b3Ee~fzI6+JoyOXa7vyWD=0Q0ETgYzr*w9uOII34YZR;I1NB?f1JF&PKD*5Fmt3#r)+82SD^7U(%>jLX5aroua}+`fB7-K zx{Zc^L`DX*lhA3@aK1EQkk$ry&^6p7fg>OtJ1AviX6Z{LZ47v+#fb)4V@!x-R_ZG6 zdasq;HfLTYbE6|HfR#@4(xE7GFf>2x3iC*>t`yPm3OVX+T7zCpP=ZgUU<7P7njwh& z5FidijWAzPOqcsc|N8FsmDOImFRRaazGC*d4kEUtrvsPREwY`XmA|pa z-e)L7sEyl6$=%SH%+pKCfSxRrQC|hx5qFf9-B*hcUm!bCBM_FZ&Lr$>b=(nDeFPv7)>@Sr9gyL zOCjJz77T&P=0uHCVKACxgJI6+bYKm-$R@(V0Eerz<~?)HJ2;i5Er5WNjbrci{^$u5 zNZ{qQ@?VX3uq z%1YsKJ7StMPyjiwG}gt!oi>nlh)|lG|LXh1Ae$#rZPuDU-$BA(Fq}Z`K zeKCCMZS#kB%()N6rPm7!XfJ}k?~VKg6^pYHUZbzP%@?jENzan|&YDBUOZYlrmd+bx zGeCt)`o-Ee7a#)Cj_~Fz&nOk~H$^}cC?coL)G-*UPyTv%NMfDG4DpJKa=pe2Fk*H4 z1u;<(l-KA#NygG>X4~a1o8Nrt#^rnN!~1c)IRU{TC+CRQeyx1I-&vFET@Dxqg3YcU z;w&32D!X5TJim8_lHJ%S3ee7r?jyA&=8;u56_23vUP|jjZnpNs{yh`=tHHsHeXwTN z4+W>Y%zJz~YgEL(Rri}T*u6`l@Ur8)EWll9z>PvAuQ{Rn#c)l42Pl8qcoxk}`Y?ie zj%EN@2rGmFgLXlztm&cNTLP7~QTO%=`rrE>!$I%<^C&0bq>68rgEMXoBIb7<6a8XPfA@1;7Y$Mav}x>Q<#Q^l4i{p z;ARhx7Yqb{mPD=T0qqRddxO?Ul8Zg3?AUxKr|}Ypw@ClDcMIn^7W1`qW$@tEwVTJM<57vk32Och!m2CnwXH)QVrAU`VvfoyVN{L2V-&?3gw404EL!eNQ-~eQgMXs!q6J@d2R5 zDZy!ml+JHg2B;S0sXw9T(ge+O0O{RZXe>~a7xxFF#%9`q-bTmM4}FTk;DqP z5QAs-rbG#ghDzW{aMWnAJ$MX;(tP`cb5R!_UR+5eZhaG%7*mjk1_gZH|B{Gu6WU)^b7v>mru{4?*A45uM9qe`kdv&Olr zt|O*#D>hd}Asu0Nps%1J)WeapUS|VES|FNMj^dbvL&4{UfhGT*Dm!6%Viw-u%1`&~ zNz~f?2K|*TN*p<<7w;rt-wm~?L3Y2kaYm?bv7cLB(h?5Q9j0XZCOR`gSBC*MiJIecTQjgnUy%Vh|Er&0EDu1b_rPy0htJfG zt*OFfRveAX$&YI`Xix{0Mp{O#Kp?vJ`djMI;`QaZpu!_N)u)y_i_`v|4?+&&flnjdM_} zGR;rmdBW}U(evNbn{}lB$3A$_HeOOi!lo1eV))C88L(kA0(A=yiVOms}N2@j*%>)ZTi(c-DeewS-*E9`B#4s>4#}G*dIP0HhmDzd*lesUBkcGd_S8q3f174j~)xi4xr($CN+D^2u0<1n^0s%iMa>`tT#b#1ENCyb;pTDro}bD#^Kc?G zCTkEJbdq^PTH{tp+eB){LAt9pr}WAeVVZmlC7H` zy3}#BBn8K=PAh`OJzgp9=TNIBV_MRqvo#-IQG-?W|D=<2gDq3dy8MZHxSrTI=Xnhn zP3}I*7}M&;H>n>TQ{8sHcm68PwuZ5cwc;-4ZAo+}2e}8(96|H>s6KjA@MLbZNe6eE z1JW2YFEo#^zc_v2%N+#lNIdlayQ zu~%-PJY2sT{R6Dl#Ls(N2ByZuE#z7Gw8^FNZX)-;zxoaU<-5^vv2k_y2A~y(QXn7g zDaGMbIat?)22bqR$8!TzR9ynSPN=C{-U^Ri2Or6qP?Pf3{A7rg4wJ7YEKN+#Tu0%%%uKD~ff-`<=( ztmQmpD`M%P6rK)oIfR2#>0QeA*8@ZTQGe7f7mIS$hp#r;1u(NU>qp;Qs~m3d5WA~J zk%o!0C(YO|eq2wX<5l4>{nI*hE^m1Px@zJppIl1a?KlB$a$)QfDdmhJ_6FQK!Wdhz zv@(ir8)3&~fJI>i*&<&uFQ1zCIeOO{MjslFX#x!7bJLZge}_M%Axxt14yw%A{15e8 z;u7sVlJ9#rc_!>Bs3SFmGWqSZ|FiT{A`qCJ8Mw4|wOH4K&|;Uv>NlmUd^YRX2E9XT zpa#s2RcD74#ch-B(Jr2cA0$_$F5ww41bjW|4D*4~Y(w){w;A|?!e{?I0;mOehNx@* zaZR5=qZ5&dF-^dhQ>rBQzS@6|NVJnq%TMiR37;2Vf6PjpmurvLQ!DO;SJwX<$x^%! z=06uvE;#ZA5OeXa2w3`2C5OWX1S*OS30)wY$8bVJ3v>mdc&*q%xaT}X+3AgV7Z!HZ*-J;v{+OEw+eZZot>Zuk1tGvh)d<|BLN7gOc@-{;;!98BG|mT33F)|}pw zd+*?nl<$x*wru)J(RiMeqhi~M($v&seR?Kdf3&4^LFvi>4;H# zBm_TQPl0}@E)9SNu4e@2gxehSHCnuSR#7HJ{zVt01n`-z@fiQXtpSE^+%~^-?_=AF zXrIn(q1aw$rStqe4eA`4ZKn|cnF8~q}>j<$Jk-Ub1fq{Q{M(=_8MU#i}wlZgvteOihMXGPWw z&1?twws|0t`g+eSKNhV*p!&^_9|Blz7v zYV*I{`i(x@J8{*Io*eC2(i~PZ`ORS@U=r+h<3f*^P?tI&pUPBA| z%vjJ4i6v*V0?qfh++!PO5f0E)!p3M?cS&|cC6c>g`vnhCjJt>-Tie6i)cok(JfY&1 zH9ADWtDrx45dEIV>)=nzVXE#;qf~tgiof4T;>NUM<yw+gp!RX#!3!W>PD};N9-Botx=(*2gvFWjdI~|(GSb8<*`ax6Is`I-wz0JMhf1^$yx23 z$ZTWI-e0xth!tN@M|4zTG%aGbkjpE_6-wu=teU*JbEPcUtT)}6rF{v8JifYK8gAHn z;ZSow6|6Tih(`}U$uBXN?H+Z6JZWh7v}{-|P9A^f-2TJ8#|w367HRiyfA@X$1SR?` zG)4s!kSpxHQ5>tn7Bwr`X$gF;*aacMRhVs3+36)XJUmkzO%V z85VQOmTgJhJE9l&AW;R4QnZ3qBpm@9dRcfbkO%U9GyskcrSiQ5KbG*sA!@ur1GH08 ze$wlKLEz2lGlGgAYKga7rqe+V_{tN)C(u+-uxm~&l<`LHE5^YkTKoO)k2Z{xMyV|E zva9}tQ^F_Jc5qg>4L1WwR#@ea(jRVKjdFd{JUoW-)C$G@AXmOrKy>!k#Zel({F}og zXPTmZw%RY*HiI7zX8O|}h(_nm0CyQhczQ_)6(<-evLqWx8S&562NKG8+$9#}Fq?~-rQe|(_q$&XXpbbs~k_aGu3&8KlwGq6WlT*Tve&%mV31e_w! z)GH9CCMIzfhIp+ZoI(R_?uDN-*OOsMlDm`?>qBZs9ISaTgvQwi4%|up1fR1#nh6C0 zRZp+KGg$0g0!uBHYrWaz=T&7o>Z4{ZX4y54ZP?uvL7h`mtvQZ{c;M*J(v&C8nGr_+ znRx%X9i@!QbK13Ocir3tbfCF3-uJm#UzOQz^IVRN z*_ZP{>F>q&y@B|H#@%t(I*!GM#GRbP1-PmvgK8n*)C_W8NJRg0hH(DUV`TZFco#Z# zKsqvB1cT59o}>cXLYsND{mdb;f+nhXI2JhW$%vd%8qbY|8;^XL!2h+a^`)D58&tm3 zi`jcqwsMrssQEAUr=0;ZnpEkpIQ*yxjvu_B4D2u~CohEVL`G`1uPw*UKP@|d-tp(? zFqF5#&en#r2bx)Dg_RVCPU_=NDh)Yg)X)-YHSId7& zY2hIXhjQBUKedk5d)6~{@9uxlUy*)eo{rHNNf&5C_G+h6{f=5YsA%faAmnmmeqKC4 z#V3CpY>VWJ()PL14Ov0Lo|3}evYabO>h$dCBckXUa#LNebL-BQk`WAkC3&xd8I(>P zvvU7b!{xMH^QzBYw(Z)z=zaSit~^zVx1p9*{Ui*N#i4!v)qCqgo$zdd>n)|p(#+@g zIE1EmF!}%z#iQWAy~UFROP#MK10^f?iF+ranHzEEd}P;27>d1`#OQ$xdL4-B(Y+^2># zoDNd`k5AOy+un~~TKa7ToaTD)zRV*!S9SP{!FP+Goc!wQFLa|mE0i=%O!4ZO1obc6 z^<^%LRU_}^ z&&0zd*GHB}uQQ@5?rCh~U`pm|?yDIB_QI!E9{cqu+1+=CiXa6S*31%O=EhSTx@_TFMh@Q&S7`zeJjW?sLQ+jTxH1FN_{ z1wR;V^rq)}6f&7TU69^9HIvHQjg>^=D80Q408TP^WPvm@VE4-*?9= z28SF-#jTAW3^uVM_(HmgKJ+6KOj@BSgPStOXYqL`5MQXt8lw1Opm{UcHh$^=R?JPz40wyz4LLnQ4lxc>RU?bbI^B8fkCTIUbI|J(xalJXQOXTX^K}&B|ty$( zC}!|x1D)_f@3s3cn%>BMMs7(df~LwBb5KKM0GjJc@7Q}t!TwFii60ruBXf0i*5Ps{ z%04mof=5}d+ccB#@{k&JRQ%lkKCjjT#+@QhijXLV{=^ff2&~Jfr6d3*K<9Yo4%#@N zr)BH=P|mDATcE)pUN!psKw<9$1Qsh-i)V&tgY4k@s>}8&rwkDi8aCn&_dTm$90-LF z*$Z%;ET|8PXLqu57;Q6~RQj4<}; z5lgXuWQBA&v_4D{-mIFNukbr@)o# zp>a4?<*kDyyfr(D?rnY+b zdQOtdDb4AFKqRlK_PAAYou{^s-xYf*?NpOQ<$g!&NLS|~kn3nTKS(om^oi4w-%C=k zb4lL7Ks>g*Cc1fn$xht;H<(X`FdHnuQ<7YH!gMNF&d=oPM7+MB^vueM-QuQHpOp_~ zycx)O&mIM#|1;)i3?fdZ0Ea@}qih`B0YOsj$eg{{Xos07wghQv%`2dr@x_g3?m>aIJ*eq8EK2R@-M(=7Z{2(J2GOmDeL!Mj$bm&Ax3wZ>6}Yy#gu&8? z*{TzNebs(WHt6C>H#wOB|CCE>X^vovN_`=GQuioGYTpjttSe_6U+niG{lThPiEuc% z#j;hRNv3sR6nt3X4-MXWp;tQTv@^WrO4wWG2XY~Uq*!SkXv%@wFEn=JiMVJeX7={f ziDh6HA7k`S^x*kYKv2BxBE#I4(){@O!V&kPvT4NIVXQpuBTF&nmR@&r~78sB;B<6GcHMQPhpQp8{||rSNveOD72mld3V2e zV5Sv5?udGWoH@Tj+5iY&$f|Q^33!|v{!tOk#s}@90H0@GLkg#N7e5KjdcZAg|7hNM z3unyr(z)CR8@UHN4)KE`d~GDo+2W~|At$@b`L8Zm1B=4OuYzu%L$A6v{|SZ{g?UAx z!;FF@1tj*%ZMc-mT{gKXS8!5e=^zR=ok^Y$=|qQME)BF#AwoIxuU7z5$||j<(!uL& zZ@N0J@P*>LC)KUhaGpp8n2IJU-Y_ZSh!UI9%W=YeYw;52nM|v9VRFQWwG8g#2 zT#QA;S1KiwM6{&BzNq{%Kj}f(>e+s=q2cGu4wKDlnGa^A^K-uD+s;RA`0P^rwARmC zCMGQ-&og;P9L?U9!O>~SN`BqD{{Pl9bY)EJ3PllrMtpbWh4S^#xz)w~NfD&ZGr$4M z+L{Xe$6)74o6p`kqjLo&@00%Zzgf0z2R%_|Al}4y>b+)3PDcv^ewbcM2EY&2)?ewc z(lq~CTx~z5UtLc21wK9UKu_EWXwK#Kv)S{>^P!Z7kn`Z?CHdx9W+9mV}phEs$UxZR>CU>`t3jN~``x9?-*`u6T1# z-{T~b4di~)PMr-HXv5XLd0N{t!+N&p@xWg2q9_=!sRw|-N^W}jz3cspZ4pXla3;bJ zW(a=lEBi7Ss#}cP7N7)_sS56s{l(8)%Lg4ZvtXx%s$+%H8 znz@LS@^1GR)prbTgCH9@bt&6s4j4n|K4_`hA&;JSz{ClQ|GQi{3>FnRt&^EP*i_VX z)KQ(KMG({u=XU`^(+9<**hBisO(v%&wAhs~+b0L7znV zp5q5`+{Qe+3!5R+puc-oc7#oqc00*!bU*mtKYrTP2AKW7S%p2D)GW0PS096Ql&{~< zxmoqbSrqfZlB2>J;!~C(js~iB{_O3~w&HzG+n#iA>R|T6XfNz7v0Or z?TH?1v(Q5%u^S2yMY%jRe%44| zY)qFGBRaVjI-RzUdMATF9Lrtd!@i>bxc}wov!Ckrx$s?FCwza!;q&bKYDAFUJ}JXn zuiX$2RFEa*1DS|iMu5erO^(eX*h|@)Z(Ym?Z`D=!D6){DOu6s z^!bhE6kU}{m}U)<4`#PM-%Lgt3vXe1Ft!CT=wfR3wA4&$_;mZJ8ae$NKA7F0Fq!-& z;)mh<@6B?syQT88{{@)y{x{kd9G7kswys7RJ@`FQVWZywgtdhabt$6V`@+28WVEW= z?y!5m&qolL-P7}5{!tOFh6IK>y;n8!ec0JK3VX0!zYJk|l!mj#!eXgDD`i9OX5vuA zfHYbbUU3RN$BIK^77&D387N8FPl*!gXSli6JuQzQ`HP|kd+x#X$uc) zgN+A3ZMwZ|#vi0=21TqPOD6c=+Z#!-zxq#eGA?}SHTR-epHgh!=II6h5F>xCJ(W4% zJ0ziMnaYr*PZs<=ka?`7e#u#GJD7dv_p!3YeU^``1OtNI7zz+B$YN%2Y_yCl4_c*D z@5ddfGT18W=YF$0atqq4dg>m4 zU$;eT-rZB*DVg2G?e4@T#a<|3jt{$3%_nvN2V2AY!tk}DZR}j7-)*o^kwMU=kDn#H z9^P-ae1^I9h(593mKWRd&Ysx@r)xw0fP(|vBopokNy?5&;q3!wV0|L6f<7d_> zj(P%k>=WPw9>w|G>irYx^~ZrOO~Yj%jxS$F<;q{#`IaF!?{m${@+|lhdxLdOz+1K) z(lxH|{f6lhAsl;O@&r+IJ(xg<{6(xkZgbTFQKY7Q0o6;sJX)WgZ39!8d(KCfXGdgA ze6x4je7}uU8Zf$vHOi_Tj)bzJX`X^@`BkEPTV%A;3^apIO)yR1E>yaoHGYEc$kyEe`KQMY0fCSVQHW=d#D*M>dvo^$&!N{fh5u3U!58Bp?$Kh> zZ0uVF>GiLBl!zJL8Q6ols1B_DW?&^tqIpO6OvY?Rnw`Uy=C|vct$f7TeCS5muIq_C zs6hH>e@`rT^g7u4(q11%Oqm-}2El&N9d|vgojy@OB1fY5BzLdz;W(>3dd{*~ibQOM zxIYu`5lB#Obe~;5u14BP)Zz+w;&{^_`iRxh7!}S_4C0e97%{7Ek828KNX-)Lj{>@_ zpjp%MQu&qlIZBz<%dR6(Ko;){i!Y27Xy@f3d4YVs)D2a?UWuenwgJHtLsgSF5s&6O zaLLSh3|zj^o9x{Z)iiNH4lIHV8TzwIepmLCecUy!jQ(AI=@W7>8ip5UYOLmUzprn_ z^L0%`h;4b)wjy@B>3rv2ba+C;hv&>CFyA{pL3y4d{Uu3CPqD1I_T$S;9Y}fUo$uynnCm78w)M}WzdCX%umw&1Ne5<0;JvkjdD?N*W^D!;ac$XU zZ22;X7Je@P06+jqL_t)`ho5uM+Zwb%k+-|7zBn0kf77P~pJY-05L*_!D(TVu#HCf) zR=c?$y4S04!4FqX&|C7^6W5j7(`eL~@WBK(SU+=mzOUZrHKNKa5PK|U?(K26`QA2> zv60vSNIEd8cM@=l6xp!8gedkyE=GWaVPahp^7QUk41bLNDN9+J+ro;bFf?>e3dR zO?0dX!B~3OO8tX=t`zCy#7d^j7U*%b!gV9QzF@rF$$hb9NU80Bo*w#W*Iei_XiK0H z{IksUCCh#rZ>9V;`L-1Y`y#2R@iuxdTn|N}7rEu$EbXJtO(L1_*&O}FzAGy8nCDRw z6NyjoG}s-UF^#J4i(%xt+#QZ@IQ^8lZsB5p1ow1I6s^Oxqf48tUQi*1u)Q@p)cUl6 zuxNJn?Y+xxKH2_^ml>eAW)oic{?qt0auhsUnOP?_Dm~ke<(%JMtvE_75#^Gf z4R*x5g|!6=$q3+icUHpAfj?4AzLAIk+T8sT+;&yOKxGyqkuEh^R1o-S=1A zZVAOFK$F`%IblpBJF3v>J3&_U&-HZ0FBjA)AkK=aZG5PLZOm+NvA_FS9&^(0N6LyG z%|3LGDE+bfp>nAY}SL0Nc)et zJHH)d@Is}LgMzhGG-Jj(P}q~e&NRhvCNEWkuPRHAFg7fz;k>m#2{A{hhI;yLpFqmC z)zQ@;^~*12`)YKqimmYYzD`LGets8Y!qv{>h<^K|Spe|$Lg$X{;Y^z2g}MQSIGCikx?@W9f#-xJ8f{7PBhLqN#g2ysNUT9n)iyVP(LVG{YFs<(0L zi(K1u{UHS0Z)H8%VVF7qQw!aAowPOhZwUab5NA-!5-EGg@(s<3_0#7RYUjFq*z#|@ zKHtAfeWWlgOrQcH4<%!wukfwq(t7o~a}ioUWva4pY!B~crgZrxGg@PNvw^{x?aFmT z|D@ReW4W6>VvyYk?c|_vn^dWaV@qt{Qp?G{bRM^=kV0ARuJmRZM1-p zZ^LzsW%g9H5od((C>=t`Ei#EziwjoXR{#2&Lj~pHx|8{U;)JU?5X^j9X5Ks+G-0505`m|v)xesyBHe!( z?*K&MGj(ah+avtCA6(Lu+l=#5>?L%)3jf(QqduyE6@1wIqxtQ1^KwF zQCb6&Am?$`xZ(jRSpkvr`3>(IUs)?irgD#xUzh&VIiID42zl$(%f z0JR}&NVYN`VR)bd;AQ~f`=`Qf=#06i>srS@>iF9YUwdyio|n3VJddyT-h-7}UASkG zB~c8}>%Da?{kN^N@`*z!BpoyWIf@N#d@{ za&4yq7$>DBFa6SpswX->{4U{{ZviFuSm27qqSL23T5=iE!UM;S3;mO0#Op6}9&jv? z+Re3HY=yko=RGl8mv>8VsEhL6mGWH$7YHs4X{E;M4A)zbwfo`-_ncC!r13d_HhND` zW=CX&E!}61*Kh#WPQUonIaTVFAViw`Mf>;C#2!3iUGLvz8X7dE z3gEtkZ-VL*gAr1+6!!PMY6XW^OdGqexN5zP%oth#^_E6q-fYbAc*H#@6Hq2k8vAIm z_LayOs5_jBED4IXiy}>LXusA{Z89olL(i_kz_w91WIvS>kDzKrq?-9GT-6KBndH6R zf0EDNWd1o9k@^%#8B3>X6nbsn6^{oeyZg5R4BTumJ@09@V%{!nD)kOoE)h6b=YMs= ze3leID|E6ah&HIi3ih+ly1u|$;SyQTDpf)OsvrjNv<4aPE5$04hp}s ze%uNNbs>#T*1WRDsNxsgiwsO_+GoN>v#oVXLEP;1-dl_2id4&d=q+;U(?b%wMX}}- z2VLG4MqBD8dcTooozHuzaBWPfP1|w5T=N!>PUzsau!RKW`lmV?Db+?Dd{B2&-fENM z({zC*p!)&qcX8SwO<}BD$CnPUjKsn*ZV1hEHQ4<$BWjki(J&j)i(_3pGleEZKTE36 zG>!d%>?_Rw`QKKn`-_Nn8}G)Gvp=jgr@`Q5j1^=mp1}^_G4~kRVc$U@k|V8xA6spq zRao>6IkQN?_`6b{t5Y&c;eax?OJPjN=+sYg=m1LIuQAH0b>I20;1x>1ClyUQ?^zE; z500Fw(0Jpti!@vloQz|<#?(rtH_J5-Xro~^VeV|@(Q)E_I|@HlN%dmdzkd{rUP1yo z9p$sZO>No5*t`pDYkDHEE(HSgS=m5sXicDf9i2lbo%oX zPkTwC_=M{d!a-g??}kYnxvicn%-ve#h*Q7zO-^9x{_2gsXQ5t`Es@m*Yo*d&q=cI4 zCIF5X4nLBGcm(Rn*6MZ-<}cbCA~I`OzL7dk9^v;9a5^9rjV9PJT7vNXZKwF-jbJ!m z8J~;e#tJ;H5Yp6NABhjfyr)$1uMfVx-p{`2`>7|4RC~4$Hkh^4EzzXSaKzq1+y|!V z8*FhXfDXiBxEJ?Dop@475W5zrHtHFSg!cJW6w6^}OqQ^d8wuq9HQK)Mt9=^wl-rnv z;#KqKu<&#nghbvF-Pt9B{l;(H4kMy9h&`B9MxRq=AD{#3%2q0gmf7lJ=mV)c2#b64 zx5V1g!FhHGbyRY>ifv6keN%=f9?p`4a6qMudFh)sKB-bttF@GATbtJhx#Y(HZ%Qe! z;&iEOr*5RZ3k~1bI9_{+`D)htU_0Wj8{IAONaU_H-N8P0&IkUV_ia(k95%DLqdHM6 zylVz<_hb-Ps^;~A{0lF*#dHkZPucIJ6E`3!P#$@McWtuALmBO-;*N9`#qbMm&f!*k z25&S=vHU2ViW8Sn`ekN+$um?1z=66&>9{x-8}oSe#4@#95+`d9NniOsYV6CA9>;qs zDtSNY)Fl&|j#v3%iC9|5vv5igQUHok3rGL+SFpGlQ%#KB;+3 zV>kD*O*oOZ@yg?2Jnq2^Ba&tg${I*ZE!}^5zgFx{P7^$HZQJ^dXdc?TfC6{#Voh}z zXc7kzBrf$u@?(Kbn-c-Mo3ito0uu#??1#ceOhR^otsvp4dj^;6`a`U zREIx2bC9Dc^3HZ@91jTL!|OZ|o+6oNK4F7OJ5`!ZNLpGDdn*vt$0qVB3x3;kZ(EkT zSZrIjVeKFf@dGNQZU$>X?iB4-Lc}SeAbDWZX}tZgtv!8q_VPsw7mD|M=omqv!Jm9R zCL^)47_5+!HUI7ZssFS4n>T$6;>OasY~ra0N5d)KV;NxXK%707FROv+KA@Vc$%mRT z7DYMR_onq~^;p@*Py_mN?8q`ffC_$G=W){t^2aLet;&cb13$(zXGB^7)&gXR(ya_Q z4?{lzXy%rWsfZB#n8)y@q_-}eFSlZlj0V2X`5z5L9EqyEkj4`IXjNLbqq4TI{H>`y zGN@G&wR53AjD(+Aed@+mG?(qEwP_e3b0@P$ZTWzU0quy>_&xL<6TPM8DjZk1#$ zA?>cD9gEcs?6k;v*+D{%iAeLlj|7`_D0tvwbTe!Xn|=pY$3C!Syp_^U9-#f(U1)=N z!{B=?@A&K?5lp;VwgJLr*-kjlyYmQyfwmcF4~cSa-E-*;&m&=AQ3#bXytJ4zYey}1 zL+o@Fm?5UuE%c?~(Bgft(=)ez#UzHAme_81>86$^e)${X9#AtuAYgp_*QcwWKY1_~ zRx?Lh_U)x&4DMDNuI)*$k*TFdS3#qjF zkl{EUw}Pd{wo7x#P{|j3toBSBgT+86=Cvr==>ZENPq*HEb$sZoz)jkkApZBE{`wHF z*gg)&pY3RA3hPSE$J1NL((Km7mG|t^CQ5a^Dmur*cOSkZG1+Q8(gBCn=y-d}QPV0p z63TiIy2HSfUkXllnUBuBV`%S0hYk98Y+p^3zoUym`1?=XWe%=2-_xFb^Yy!X0LeT% zB`{rmv+DDfCYTa6^u1OJd+T}Zn#QD2SM2IYZ^gh1CrC8tcsEsx}k9S;A@D+sKXHwSO{&nRyr!wi2 zg+qckXyNKg&-ckPm-;DJrQ^yM_U!zO8Y}si*`f%%FcxRrjw)U;5x>=82Z$|Lm%Kx( z-Lf6cE1t~r5B46cT~?&s)aQ-{Te4uR)$7TRNREFs882*-QF6#VzDmu(7QAS`2P92b z!T1~vx0EU+-d>{5rW6a(!t3hukdA2w<}BKZ$`};5?gXEBObF<#zV_ctHFG@DDZ|rpwXxS48701J9(pXR7DcgEize~Esr)HRzRmi(a4~5y_ za%k%Fn5yTx9u9Wq%WvzSe*Y=yUzZKkXoG;8&lQ!(A#$~FosY~Ng>)BLwca2V5s!J^4e2#8j_qhSfz`GpS*m6=vbb<4rE`}{bv_FJPAw%4J(qzv)r#@CojH-oh<3dsh4+VGP~ z;g&05ZYEn&n&PClWw^(baN2>fAHZx4@6;1AG^9igLLpy$T?F+vUySf+rh(BF2G9K; zes~C)-V2iKb75w6Qh7ZG?QXi9CAu-hHRR6uR{5m-B)UZ5--9k&l%%RBqKS+&Hm5Iw zd8~AMCfGyy(f#&%c>f0kg#bsp`;vy8_A}mwpOgGU(`Vf{J2pe7a<(3nqzZ%vl9aS*Yo$ib8ltX zL90@mMV#ZNo$+0pZnO%s)png-sm;6{Lr5}8qbpAQa*5QX#(Iup8rDo2m5FB?O+vmp zWGNp^*G3?c1(e03F)-3Rw&VZ@0Y}3KxU*csbd=1@z4*_SSC*sMXGA0iBcX2k+LQJx zJx+zAtv0Ogex_Q_NE4a>%w}RjWp)xt%XY1nCE{VW&rF@vv*@SpFnx-oPxYyjU(hqH zj2`MmqZ|&d{yTfJ+8b>xy%}szpMN6yswcUM^t(WQLp@8e!$%cZkJDhDmn&pK&Z+lE zksEXMctS*~-1@E@ig8?UnvCRB7wtd(qiC6*sS;lW*IT_l|62Lszq9Psub?L^sVH_5 zaYJr39}g_J@3s-Ug4RBm0CqqG6=oXW5C<+iPI!eKXR+kGIWgyg@sNh;DngDR{r2Qf zKQ7+OUCvCZYb#1ks%+zixxwbL7K9<_b*7mr0_2l516z^c)`25DU9qtunrlTK*YkMM zk`d?s3M{@U!$4K`mif@Eag`I)+)};^vmUsj9v3NN#Imk?aw_KMGL<%*_njv>fLfg{ zp@}iqInV>hIu7m*Nb9hswgaxv;;-gdS;)zs(~mftlHN=JI#&~&Uoqu`u68aPeNtTj zQ4)Qg?gEZchk?;>uva|`us9!wmtK584b)Imb&E+lqf<-O89Cp7j|L7*(V2T2G%a|C zy(ZCTk-F2Z#)vEF*3vQ}XjdI1u*f)<%?$PloMIaPK5R;+pG&wOWps1|1N_;pyegi! z-cUNU5iNn$BTA;SXJ??=*geW7B6Q+Z=@|W%9Umc4p+h>FQ$hY^v9SB2NU9HiZAscc zsV!!Q&p+g6!+ksWjrjdLf*Of_dgLyDNR4w8L=s2Fj$jt++AAQD@d|CXrVOxLGQ!kf zF{C=%;hvX49*L)akr$FubfE=VWZ38vJOAxJW}{|CS)fBsY|@n5ic<;qfQI2G{#8}7 zI6hjc!9p#CYm5kl4fl$|W^p>I?Ob%WWav}+ua{iase=kh=yc&lCS&ug+;Px~B^Sd- zuXIkf^SA@_hvfOyQSk84JeMAe#_Vn`wzujoED2HE;Q*pMVJp2w-YVWEX*SGG*i@@d>5WG$qN-#=Cl9YS zyIS-`s-hen2KhsGCmA|YhQI$Kp}Iuvelhxg< zLBLHd5k~XZvj`$uNL&Z~aetQ_l=inzqt`lR>lL6dUW!`Hy<2N;_t9{goVWjUgKHXT z<5&9fb}1ZpA0{psiS$E7an zk2(IN@$=E0zX~`Q>U=9?gB{|!^*LI5kk}ngQA|Ur`jR0SX`@{ zAsHW!u}(vzeB>*~*b54Nbv)2or(LxDLSJpoz>3E^3J}LcPPMzZrb#Qe$LGTSpYN1k zFXnZfbOtN7ijP9zvHev&8_Jv&GOiWb?IAJZyGvUgV>#7o>mV;yFP~|?K2~D3EpP!X zs4s%X>s8i9@1Ed}RpjT$?(_E{kWqG~!kwUZn$28cisouIpMBej?TH$P+|ze#d8+V_ zKY?YgQstYg|L$vuHX)z#a90_)b-a}p=mKc~LA?mqyIkv;FbzxBt?v_iqX5$?Irj40 z;sB8+yWZHoM>Hf)YH`8vE|`6R0fab{FhqaR0#za!TS&suaiol#vxzo|H!ZL*c{3Gj zHzmBtbG02oB3Vy)IVobq2-iBx}nfMpp<+;+L~e)+d&$;U|GWI-p=Of@(5 zKW%^SdDc(W4$t;}KgS@bx+Bt#*er8;YE2u?cujaWZ>HA&B>MI=v zQ4-PZeU=q_=hM3zNgNgg)dls0f`La^g4xr^5M1)&%U~b_%!!;g!=tWScV$fkC&X!i z#H%VKZ-0@F)x15*AEN@@UiW`kEWxAI!P&r6)l||4mnSbz9i`Qr!LYIuVSC8*X3JLC zY^0e;DH4=1sjMcnHQy|6Ai&^35@vrHWNV$Kn2cAFr4!sVxv2OW%A#bmrrNR&PBFRp zIz4o2+*Q(6)HJ~lx*T4H)oey&o+J*m<_NBVL!ojlEO8$kC3{yqkiw?yJ_UzzP?8mkSZIMl4?WwQYdgc^=ntag+3Mq$aibM#jcr& z50CrX-<08p{FqF09us7*$=?xbtQlMg<(h>mD?Pb;(h0pV+&9x8A|wUw`_=obwnHu6 z3aShGVk0?ixzq1$=Pmw-3YBwaxUMTdBeu5FAoR({=j+pDy2-lV9lmlqHaMYm+(Fj> zf6PjD%(zLfH-btIyWZ_uE}lZBGAO6GNkzKpB&B+JwG)1LFUd~-IR41%uVs|SF96ls zZFtSs8V93G)eJxqAiI4yKy69xhuX{DaI3g?>SX)_GCna^FoSCAr9hb zsExlRZMTf@MIx4z2VS&m$q@tJncRn=rD!4x`1|Qfa|HWt@Qo=&T1+jIoXLAbSkDJ2Se-f2Lo86PYS3>;{#%e z&*(vJ3>JuWD5O!nX+ve>35_rL)1DlMYN!oA9)jvEvJWBaxKI9yojc%U2x(-QB*X3c zot@R6Fb3^juP*LZ0=G_8yr`A=_j6rVCSuAmQXQ>f>;C1v_C4Nnsj8G2(Rw7ZFX2ejP)wAO;h?c_iY|Ox!)r4hb zjo;tB(H9RUM$Ux_#E^GG*s`u@I3`MhfwNe4kSqu4aL`VZ&r+>F;eD=a?5GO>?HJJc z2NedvmMM%h&9wdl{W-(7{n`IkZ}Pp&I|eOV$xeeHTh$x@!@M{lZLBN0e3)G6YVLQQqU*vtxzOz<|aFbBT20iyyc(wJ)MkhZ1x>~9IjH>u=`Oi4JP^Z> z*xU~rxT8^f^F=!B#m2w;uOCV=X5a;g1wKxkv}eMJf2Sy?fALY?R$m0mgG5=G4E*Uk z_b1lRrQs5Xs(TM6p~3j*Wh*AJ6Qc@ln1>f)_sG8IXrX3JZ@6gzc2FZDT`xhu#FxOq zNGp|W=(+2Mc&`k0oatG@ODt&5+NT)Z-AZ zXvNObda-c)w)kq$Jbk3@7c2YgNu)bJjlJT&LKFprVF`S=Q-o>zn0m7pHt9#ERy zssl?U+RxK9r!K~Auyc0b#ltecLO%fFxt33V^klRVvTk&zX7T55w(kqR-yOSBEq{Md zf!EJp{gidvK%gxdjpV(t_wSqX3q_Kt8uuW-TgQ^QIG~dqF?s$~{$Hc|EP+?HQrXpOMN8N`fv7P-^Qtwuf(2wr$m8JizZw$Cl*lhgQZ@Xa9 zR*q_~TcL;Bfq}YwVy*cyWqLS0ncw3n%(J{5_BU|DfN3Pb*jR?_?elXsie?YyI&{~h z4#pm)WX3a9#8N{7y+$g1Z2tKPFub<7(=fY&Rn*?{Ac;AhI*s5|N6Oic5n*^~5=GCB zMnC>uHZIC)C#An!4@XU$9X?B?&?66CaPluFjv5*QMRy*ko8E3b?Jt3?NM;LoLgZFr z*4@IZ$*ytBYlIpIE>fJ@rS=Aea?#b^iO)xz-?B-TfKj%;^5;+}W5kKcWZRjuUfk(< zF-(RJMZrgvZSiIwTz|kD%0wpa;7nwWgSQPe36Na*2zI=%WM=KiVIV1d_IWB7J#h_0 zJD+g41?JZa!ZFD~bKN@McK?w?0h8qVS4Yu52fbkJ_Qj8dKP*8x<7KJN_xJV%B}a-Y zqLkWh?XR-lz8IOCq8BeeKjiWE^+W`mQsVMfTLoD_^Jk(MnkP(8v)bipvfSMM3b|+vm-M`a z!!P)?&>KZu`0z6q2A)GG-~!ZP7UuyzpQPiqE2Lbh;&=D^HZaud)7x(J#m_uDnQja) z4Kf_%%ExTRw@8xigi9%HI%3=vYH6L&cSmOPu^ckVUnycH#jo5X+^qFk8e^@^*0S`v2T!Il-kV9{l8CK& z-vOUew6r}*R)b71q@wl4u{ikZ);EX63%xM+5Z0rIr_Nw_pH}`(e{}JtgMtSldK#hEur;_Tq>ofYWAKs&{`tz!~?(Aw(RUdXbeMIN}m`^qT#88w* z-hIL0k_cBF2uDN$X(xZ&P(NW(kC#<2rQSE2XUo=`?wLrnd)3~F!@yyB#B^B%l~zIZ z(ZM1kC>gTAlCtQm=IP9<-|uL7)~QJMEupdl5Xi&Ad~iFwy(`JsJ=kr|ed%L6Xs++H zUea@p*U?Tm&)4z!tK`G_j_;^A&DJj-wqEBppZ#`TD*ZTW0uSh5Y0|GX|wXdIYyRR2oJ|^ z$#hGSr5r;Z&JZn>3e05PTGv50Z|h+}T(oLJZA=?qyq{a={EauC>s~^33@4W34qinK zdNY@k#=w5iGu52Q%=`ctlz^+-DV;b245X3ue=F4FzPeVEzxmpFrfy8@0$ zn(5ae9qXa2pXSMva9kV5)lwuVwndC2!=^H%(dbiB2BQ6Ii*MgkrMlCRTzn9iy@uJ~+dFGx*^B7tjlV80fZs3bw5<#J+_m_rCe--7f7g{VV}$R=FNA zSvtT~DF&2jB|u^(zDy}z+X<`bT-PQrKkS1+3gTsyjjJOGbz{^utDEse1|i>UTj$^W z9LAT|-}{erKtM$Y{cR@hI@Qhh$nQWxUAhYQ0Auo-E1sSTiur_dJ70TO;5(Vy67HBV zaUT<7_f`4$=VZ77A#V`Al5ZV~+#$gQC)_8mvv=khyeqWJ`%dCK;i#xNn#|eWQ(vJ@ zC$(N0kR?jx%xd+vojggZH3oA z3VedPqmz%%{&ShM0@%pz=__FgRJE43z{lUY;XfU17@PGN2p3;G+~xhf_}k0htL%p> z>uUU`ZB_QWg><20WUxlBm%y7uTQ#Bkip?;C#>9;v1hngI?q)3y^zDI26eRLDN1t}< zQl6f1`}exv*KP?qaFIUrjIJRjzY_I*t33XQ(41>nCv3{z-dS=GClqfb@j6g37i5EH z$p?R*KQEtLL<$;#OQX>Yti$r~)-@@E`R?@^DTY_!WJ)Ry1yu75k#d)0NLg-f=$#la zM1{(SbE`}J-7vuVBaUtwsY|{Ke;F00;GkM`s-E%rPiwOxkED&FL!6&A3<3TE^TdIi zmHDl+FGP(!4N{kOe-~zY%2(@O?wx zRl1wzEm0*!;pUR~Pc+El_#OUjX_n2d#G9>}_reI>bRZ4&qUzdtu4g~o@))+$C)WG! zu8LMIjxb3S27|gMZK`d2`-A74>FwK_b5&!dA5|I%hd@aQ*n>woQ2J9u3Z_) zv?`JZJM}Z?n9oQbFi7H`AYGMb7r8|g_oIJkMTDdDPT2!MW9 zUj}XNqq>|o^*pX{IIO)KtCGw%lL#WNz+U)ic z1noRsqx$)Q4wMdlyvZgzrfZ=8t#bc2--^|7n_WG|TQ@^!HD`W6gvJ4jn}pVPs3iPqdFSLrJsoUn~pb3WsOaL>lkH z6>pAX@4Cr5lkWQ6DyrU4?tS}c;7bx6|1Q(hF3%rZZ6tP1-OLQ%Hs8TijM> zG3ZqG2!yl?Zcd4OfY}E%RslvXGILGl8bmJ@$D503w+?#DnLG26Po<$hH&SQ2%|_nr zZRGOiC7m*pmkh-6DE2c(dr5s9-&78#PnTez?jAqy9ff~ff7pHAP5a|{QXdS^} z`|KDN5u45yFAWInKgWh1d4DOpmo4C+I*YrFHp-aUo?26R@M(9dzY?xivrmO_iVWe% zw^y`X6HSO=r&M^wA1xVNb^Y8vER z^Wqb1*si=30GDswjV9$_r}ATZHDnvj4^->P8)53Rjow-kG zco58Gm%`rA3PP|xgGN6vE0pPE`nSKY#RxMipj(j>YAc;t*}BhGty^z>Nm!m8?(fa$ zZYxAb52hIk3%Eipr@boCdb8DMZS>%fn91wxfz6|pXwTF~NyA>&Oc5b6Xm1*CC`Q8% z>sQ4M}bf)^8j0=K1*83Mk^{v-V;NyB;xa&a#QDiJy^58e7K_Zhl!#u zWu$d<$oyi$L_qV(e3^A&+|Xer!6`ggMYuWb2cnZuTTb^gSG205Y#v^A(wodK0deIG zkop}>s*`iQS$0`@G4|ABA_x=nKC20t$zS&9Q*E0gAuM`BdfvIq74c;mEgl5SaOL&O zTco@FiunPT?A%-&O_tzV9^#1r!I>wXKDY9fd)enhRqi(Zw4Fgv#E!tp z4d2S6R+ysVP1oVJQO13{O}dQRIX3>Z6+2&ymfhDY-R@@cAa(rEOba7fh3i>W+**R+L%$djIP4Zrwd_ zdP=MjsW8O{0$wKfxgY^i!ID?k4_>*w`zq?g!@&7tP9ofbWkx1998tOuzPepv%iCLM z@(8W0NY*PwYE5q0M^>1erQE65>w{8b_>2^LAdz<<%7D&RFF)5OE2YrZnzjy`Uq+|x z(TcCcIYt?k*2`Fw?~bc>7|$D6$|#uAA*I7=UG{NQDf9OuYrGzgh@>>31$xdNUdzAGRa_a10>p%V*w+>1n_oz0&IjCR5g=MsLJY(`u-qf^etJ8_AAzRMzA-ed+Bn^a8etUAXz769u5O* zcq)CD^-zQ>2a#vbsl{IbI5>StauqpcOY(P-hx!GM4izxTL!iiK<|DazL_e5X!yFUE z!j1sRZ%I{wEE$sE5@*fHopR+Y1}zt1D%8%-YTWGHdpKe^1XaCrlr5k_t)Jjor;OY% zkQ3V|O_{;IxhWOmHp~SnI?h^V#PC`*SzR1oaL@?SW}qy^4cynQJIg&$3083}I4|Dk zJG=o{ln_-Z+g{V zVP*2ytmMnsQ%&ZyYAOtx7lX`G;q`s1lHY;%>`CHASdcDok+ z3#@H9e<(z*lfKhDi8FC#O9K&{6vMO$Wp8`_r_YazWxWwDo>rwut{Tleg2RP5EPIQ! zcx?=aWs+mPA*CkVvwr4?yoV?euG*n^^rc3`I^RNhOK2WfD$Mfm1D*rT1T0^bJrfAY z{>)Olq#b!2{Y&yd0!I*zIrmBCo8L0(S}$5@|LN|~@Rv0mg!A?I1Xl&Pf07=?&UOXJ zaIJL}%V%=2RJuqtGSk$?SrPQh3KzqomXB}S7xAASM0zhU_AxUddvmpvx4X*HK>5vg z&C9f^FHY_mFJ@v4EWD0PGEj;Q<1N;@=WT0`^xOSWmK7+oxCoD}fmY z$*U5}E2{wYtOutXL?o%AOhjC9e#V{uZ9ke z!<-+UiAQE)tVV;f{lkL9XfYba@35<06gLt%?2#+IR9hZ1l~Rr#0doPLBm3-n37v*v zudy__dx1Cm068R96V>{DWKlrQw_EO4uZ*pCdVO8qIUJX&ZDx*W%1e>O5yf@-9};uG zTad+|7M6BcP;P0g-IGVgv`NlN!15^Wi}a5)Y`lry^geCO`WrSDYT#F8_R*KH)!Pc_uZw zpqt`R0*5K(N!V&?(a|ZpFh^1pF8gbb!*AlwgcMo?(N^7R8$)IO#1(I(-|&u7t#3z# z&eK*i%yC^THyP`abC-$YQ|c zO8bhIV&6z{j~wE05fyZf?E*!-s(2!yeef<1i=~A4?ue-CheF+N?w;In`qsEQj=?K3 zQhJu+E>l6&ZjXtcfoy#&2IsZ7#rcc4K7U4 zJ@WlfoS{RJvbcSDsqEFwUDe9Ax?2O75%VeQN6$3W^z7STWl{arp((XcVy&N3Rp16R zWv#A+`=!cuPb6kRzX%F4Xn{>o2_4Yv`wCu)ExMbS5rzgk%_I%2GmeU%PG9% zeW>=WQm+G@Q2AfRdAC#kh`jv}K9|4QdxOvWY-E3oCqvwP$e%!1x>+<(G&B2$fTTM! zk;vi7g~b(R-7+qmN;HmxUDTkhfk@_Ak@g<&kq$sB3tS|%7^3VjSK7?^&PQKvlOSbHkrp!0)s)_rr{)u7r zzUE4hiX7QWaY$yYZP%s#_G>p;Fs;YCy=U$rey*x+&PqwnT!_W0d)rOG=yN)$IEq2D zc>p!%e&O%dw6L^C0IDp%(E|8PawE|gZ|gH?Y8^@ctl0?OHDX!+&G0cTy~}Hk)iq@k zjnC74zzwZnJk8j`EQ@h4c1pt{!A|1i*82tUR>7aH{_j&KQrw&FNt57X%#Gr)Ct}$! zD`tfiocibgNg7}Av2JZ>+UmvSR0ZQ?$)KI{2u7I&sS31#0q1h|)m?U^o6C1Wcr-g)@Ee4IfA?@SwFanG<% z7OHM2n}iQk4d3;M%&|)W*|vdfgw;D{B{boJj=m}x+qAWe$K`jK5#=hlacj@5wwOnG zzS_^|Uh|vqw0=+&;_IId1SuQd>(=J-6RWq2dl0hI2WhMfs5RhqZca!fi0V@wyV>(f z!MnWfAJLb)(N^VxwLncO8E2XFNcRkUvFkjhb@|-kHh`O0PaVC@J&?Bv+&12<=&1)$ zT@DmIB5;Q$t5t^^t7e_v+!>qtY~Q5kz&QvNh4KtCc#!W3=1$^D{5g_$D`fQDt2AVK z_$i}5aF*=tMn`-zpdNV&p216-sZsY~1Lrn#002M$Nkl+wrWPt?APCn1p1+p#ag0E>bP`T&bgo(060L$zx8Ucn zzH9CfjtWsGn*lWIy3d4p8+h#1Rcqjfz!MAiVlIACte@rY&VTv%`57BJsZYh-YLu+d zO)p#qJH=3=OrJYP5{j%oMXM?pZC|E$V&4cUeaC=>3G=4%q{%%~c>jZHg7Ut6@oTwP)P&cjc`1F@V2LTdHd^a9mY@cb4OK~xvo z8RvoxW9spfPt2b*!9MTHgVwI~k@WK;!G?Q#o83X66Z#4aB#L+fW$MvnRr{03bz0t* zf)$Z^vz|Y6m#_J;PScyz*__p3@3ZOUWE=d4WCAN!t|OcPE}0gXGP6ZV+SsaQjDwQ? z09!E6iBCM}WYnjwu=Di2b@tqU2z9r7<9&DU+0oX@7CYAu4}rQS??g!_z6to^f_y+M_76W%)wTDJd!Y0UWo3~B4A)AtOWgCG|Z<8KjhT{l*%M03Ea5zO$W2y$HiV{Aw z;Zx$PBgMEJ^JBVSyxJ!5)ebD;n}uIjld!aZhIoMwGjGyeZyfK%C$oK_?2NP+Di5ji zV%GM7lJURi@Xxra4Gt+Y5;*k7+Qe>R;XX)88h%a!JVPspKChbwpI1kG!s z{)sPmTa9W%_+o0s*W2=xff%awtHHwA5zm8@3&tZ>@ZN%h9l4(YC|$GjGSx%C=PGC; zAtpy$siRoxE1R96P&Tf!iB@7cXL3jN1{g|KuD9|)Npzsu&Ki;KGjBu1+g3+*B%c*0 zQZ7Ez+u43e4^cZfPYDFd@&edNbq7s_2?!@A;;q~ts)F7<++l5IOkYIX?m1OGJuH)H zzg)focqgYCQ$hkKy&}XW7<19{`lE+!-56}cM`rG0eikxpngL`_;~4HQ#QPiE2!qiSgOnb%uXN(>~JLZsr! zk>e=~R8`fVLOR~G=Bm(ja0uFgZHZ`haMMT+us(=+L3L(T4He}v8x7DNd|a&FMD67C z4|(f;Nraf2!{!zB%Zk(-XIuBBu$r7cQU3s8uqcO{c{Q3$EYWe*{P@eQaIMDOSzY@Q zdYYkE3_mrNtK$KY^$(AxZ#_a)AM+K{BzGmb)aTu#G#Ej^c-0F^=#48Mg6^@%h7E>< zM05n6nW>R#*+zv2aP{iVQZUk{Pws$3z_DYRbR9w*qA^ga@R{@2NDph?deian7I4Q7 zekbJ(wg!=H2+3`Atj$EU$?c3T$FCM?s~Ep{Im$NjlVIEy?pP1wD(k9p?2U#AO!d}0 z(d9eBz-Tbq#6O@xZJQ|{HIVYC8$;M^p?#kHxTQmjS!^i1di4izEzhs)o)Fq(9X546 z8Dn2}PWL}0TIVnw95^abJVq=6X@PqU60&Z2d2(zlt)WL-+GF}c-rQ@%!g)v(jXw%j zrZIoje~$p;xnXz4JqdGcJCIG>wlL_&R?3dTgGo)e)jm-V)oEl zdf$T&-te-#-FviQ1l`c4;(Q3l3F=q+o@Y9{gfjesg--vc{g49g?Y)WwOp~I`N|VQK z^dQ;>=D^|z-E||fE2V6@7ppYhFuoTDCtCFo)kaoE1Eo$me$YgTU};oTW``(&OJ$o= zRX-nvRKWH*DnsbWF9y1x9$uOojKSRYx%k0&fm~U&4|6;@cGeuqfv}Q}Z8&x=mfbLR zI<#t7sXwp6$zT2Kb+z7a6jSt8D!TB9WdwJ?RXyBKS5@4HcF~BOh5PUUCret;tnk*l zf~{*i2A6r(?A+dU6krNC3gB1M$)}!}a-=iVQ#=ZZ?|16(P567%lg_P%T#@kDI@ioE zKTv>eg&|o5!aS5Un;f+-Ng5G4fIuK5V6l#Wpmx0s|Ek%^VGM{_dGkyS&7JWh14dWq zB`2#m*!}u<0`C&NV(wQdN8ZcB=uN4@v>a%k)2z1k5>hT$FFrJ?Dxj*6vHWp|d0K?6 zr)n1EB(=Q+8&rdj_nwQ!@MF@y9GgKB6?|0A**M62V^ACW*0uPYa^1Cv>(Hp=YIUJW z8hB7+&tursl{moQ1phfKFhBRC!^hyFtVb+^Ci|2N6wuVE7*PP>G1NeFO0|KIzuL-4 z)hlyz@NJhoKAx9>YE8kBs$oNhMK|;v{2|nHFB8mU1V|Wb8(A@_Sy&dhycCzb-Fh(j zXRTM{UzVM_Cg492y8|eE4i=1Pnlik-@)NNQ2)k_6nhRu$UPEnNWv~9cY1SV$JTxz| zQ9b?khENc(bOy%#k*c-itT`2m2O6@Nv`DhUiS9^O5Za_(Yna+w@q>AlFSa!ny`%bY z;5YXN=Opd8^3k5+VPgjFlh#|f`is3uU8+T`P)Uoi*;8q)>h%LSaX`a1i#>w6%sND5 zu3bj-wv`1F;;d34UJnioyM*#Ve={)(>GLhNAF}OVz^KiG*1Nt7dGY7yWV1pkAdP23 zd%N0=COP%tl2Az{)fR+lQYeJb9K9k$)9K)qC3)T(PCA>_9Mxn(4xtG$J}FKfyYU21 zGqY!*OHbvW=(y`qW5MsyF8a2iq^9N&x@-J`Uyg*o*-Lhq!lF}Fo0OJh;5zr)3wAjG zoMD<3%WzxB18U~DN-44;{?LSN1V@c)l|ITjTe6n)-;O)&$5^y$QziNYGh2*HS1C;<$^zul1wQLahOH^1AIDZQtq>?8=kQDwntoMxv-mmLJWI; zvly5KuhWjFu^F|+j%2C?Yg)5*OyUS^D9}6Fd}O@igrI&k|G$b(-=Ct+X4ao1*0Y(l z6YkzqNI4&nk2SD}+v(wlz)Qy|NR&JvjY=X5kpTV9Y^xk(b1e%wn9Rm>7a?UKwn)5w zcxGxm{q=quAB`E1i}weKncG2i^4NKFeJ7vVctG z{m}x;V6*1-E|#XdZ&#R{Ysgzw>N~gLNO8xX+b^ns`2n2n!yYg7rCwa}8(O#lHlQR9%J&aLC#-9+6tuG0GwRtOa8+mV=w2M}QrR|U(j zRvr0Q$v=6>RejhO4%f8b=?f>OVfRjwrO^Xd(;Emp&)x&ylTGx(ZSvqS^bj(*jgp!J z8sH#_SDDEIhrL)6pw!)=pGG-)CWja>q z?!_p2F|?tlta29BCv}PEm;{CG%Eq}Rz?LKd;K#sIzm zx#^HEGo&-$q)WgQbc@8&!`NqLyMs-by*!+J5kn|s)I%Rk-yPuCp_C>{#K60vW!6&< zbV~B7NHm6$ylXX_2}>>&YHOr4-LezCd6K)vDhT?0J|*$sJb7n^gPfbLqznWxaPnt8 z;8U=k6NGT7E8@u`i8)XdDQxCX&+V;wn!JCgVC(6TZX4quJCI^ivfW<0#O;0y3N{F2 zRVvBW-lf{O4Cd|io(_;KauQV=f--)i`|P=B&UG+Be>M7;cz`(d&(vd*hkZ{-uBL5L zurk8ZG&9OvT&_hb`e`B$9r_3TA8oH{`GnMYD zj)1#|6hu*Xg`HNn0qf;KE~bl<`{jeN>}_1$2>xq-ZNK9cQ$VjXAc1-N2HhNbloj2$AmpRaA_VK z+HsBudI0Rv12${bw-NF##ydu^87pk`jpUK?9DWUlDvYcCGMuyyy>V9O&^%i(JEiD3 zy{vz&rCW9SH`nqN!k%iUf2KIi_?tTs%ud5~cDRi|8ke({s=FCbuFNfoC%7_E`!8vu zgdg{yA!WhaOXFTEJg%O;aDBrM_xBtcSr_Pfju6(^M(VvH%pKNMK{`|S&x(gLLDn_T zoqNXD?Eki?BVqlxqt#U3>GEdRpJU7Bv`B?N>uwo?W!RQUfptG+=8oap;G|%xdCuFm z6*JCAyjgsLc#7z#qSxai&K1iAYV5I#QWaDcJB?Wzr-m`h0qxbZCU2vs=WcI)+!f|< z7;$WF&H#dis63wco$W;akQXUc*%H2SlN#(Yct(|Z@sL@XTMFLR49t%DL^Bv&%Un+J z5tvq=*Hv?wL|ca7mi68BPJp^0wSB>Lj|$m$Ec+_{0By=`aN}?)mgjNNUfW+h>ALU! zJgqu*SU`V4i_#sCym1nM4jDwth*9U1(xXNyc+_c#Gx9WLgPQfQN2JrdX z0&%JE0$0L|*jn=Z$IcGNo)8~`GdWp@4IIDYnj8^(^i(QG*ZJh>^$sPo7;m&g&WEgm zRwU*D-wv_s2gz8id{J3am$2decqUPcUSinpn@BUZkDml*lUu#rKr{DFG6P_;)#T#a z4WC*L?Q*C8oFAVl2$6_utxFeuR1du#^Pt112{;q!<3@T{61{|c693(mbG zP`p85Jor#Y@hHNI&^Xx zN;82ZcvSCe_7To5#xYK)hqOJJR=^_5-xFe`+$V?nPT59Q<+bvp{c{1%20d3F=0O(E ztc3gqlC7db3d!ZDKTikY@Q(h|Ez{fS%l`;RlcJ`Q^;X9%da=z|8y$9>{{SsXzQ9)qBX{dmp`}K>jJ}RwjKd|_h|d))51dg0D6p<_DAEy~>cXcuQ&+Se{fKc05fM&T8khf)(TJ7fBW&meMl zg5;RSzzUPDrfMD_GUa_G^`y-K=Wwgj0OA`LSlE7qyP$16uKqS>;OiJVM=L(1l65Er zWujDS#aCGNB7XDe)=(Y{=E)7O=acY=EGOem-Ct1^&9`zdO6l9G$wc`W*iFKY?A`SP z#qeHOxsJFQP6lN(o(>w{;|`dTf93)K6M4s@zI;TkBRioSJ{s03X*W9Zd{kwFZKY7` z%LDLphY_zRyVbr~hRwL&X`eT?Uuuc6Dr|bp4(o2|U!LZHRsq$D{W#TT%^7mmYyIC; zdGNdK%*E=|(%%Q)Pd23q;r&oL6eMHUmprbK?eHL2yFhb1h+XfJvE0=eckw3nv)iuJ zPM#L`dB^eJWQ#7OJK?ylCqQ155JLgD*MJ6ThMV(CfsCX`Fcc>3F^vtNLHYlfjDf>VV}hU$d2qTylRj<>F3 zK9pmhhMG3Icp#l(h50~? z8fk}{E~$#h%P>woD>K1auFREuRz^&Li0w z_au^%ZBZM0t?VrgST38rE69VGQ8+6ECo##-aD=JTb?0yWk$lhuq;})b0o_`i-Qn7AqEU>oBHp%}s>e z|9B{E?~U_uWN;~jbFESblDp-Jy<4(`YdP|fR;@|deCZ56bDnm&KGtenUY$Bd$B&8M zPj?n^Q-+tq)lA>{Z+csEkJC%zsm~fEeb~DU-6i#{@za6sGE60Omgo^%+Iewhnx@4z zzsql1eWVspSS7_IULmu}!SHlYIR&@yMo}3ca`Glm4TyM?;fToxW`EQHHj!|xpA zytnMH7-AYANS~VwKKXibLBka?ELKttE4!r10mG#Rz*Hvcxe z1wpzub0azqf$GfDy)_Bsl#8FTWnaj#sVzs4h4d`c##FRhbIqwp22fosA-vE~;gulQ z+`CAMNoeHTh%06eQf0Cq*igt9+kag2Ex3FnHv4U)VxGvm0JuW<-6o+L{>f#3jF4P8PZ;M-0G2&vWqGqXU z!2i5YYZ$ ztF>^)ZdvoCIF2<>y@PE(bV8iUhOQwq#Np#aOds~r{z;Kpym(}j*f{-^^?+&$!-CZRbL_^51TZ5fus0jI-d{jWcMD9xQN!iS_KzPE$jw6S`| zniL|GFAgZv-0a@&y?_777Gj=lz*O^4@XbOjohKh=(+`k`-21#Vx@73DfRj~+k~gnN zcN9bz+fl6s=fM$%Gdwb!r00L|cDM+I159pa6aeWmA!ef~bmEztAk8rNn2Opt8CKwsu1nrpASuuaOuX?y|AHDdsvqDDmx!`sNQm z()wejSF6K<25%u<_!$LujtWgg=7w`-!L-O**X|Tt98l@Hd(~5zhS6LGTXeXIg2iMOsuyqOgzqMs5&M zXPnL=snMESkYL?F8a-SZ~zi{>+cDFellL!oI2fqt1!W1{!F6~`#w4n11h4LPT z`d#PxmuJ&;nGS(_l%2?FFCEVB9K;Afo+d2TB+_yU6eiy?j_(>SzWrBA@r#l84|j-N zm1MCUI^=j@MB4oyFo}JW{jTAjz}rfREz)Dpyq?uNM2Di!vrSL6PQ;GTDuXK;5A;d> zNG{m&P{GiB^-vYA%ut#RI0abtK*}41X$g3CWIFP32Ye`dcnj9|s8$fL$Y1tDHEV@{ zW)}qpu^leBqY;Pj{5*PSq@^`6~qu%pA)d zSSRb2w5)bv2t3ehYgV4EH;y?%L>>kKLSDT#Zz_LW?g=78Hvmq?E+Lrp7ZD6JA`aCN zsSqitGPc9*VT9N;#QjaIsR9khnNNLHJZ>cB***$lMG`l{fuw-JNuWJ+%~|vS$8##? z`$nW`#rUrikZ+p|M^qY}!9Bc+nrA0&s=V~6W!K1q)FyMY8;zWhl#sQg;bV!RbM|!i z`JvR})4mX{YvAd5(!`282T7Tn7)BsROTM95YsF%eHRcTFWLyD~8h**;o$YLq&e;o* z(9P7-gO>8$;>BGcCeg>!%aLX3UYpIQ<%i#);*_mr0pF3#_;CEwab0AeUi`z(%5q&< za*3^7gLE{Kx{SA>D0MynCarmip3C)Di=xqE!VCFhHt)7F7P~ZO;-g6T9}j8(+KBb{ zSCk3s;cUf6MVK9%0x(7Tw?8#6XR}T1%m2e?wjswsi6)Te=|i*FT z8`w|IOTq&G^iRL}RN1_T@NW%s&RLFiRU3C?bu86BBX5dGMYl#a9n(eW|r<(aCx}Xg(EA6%^H`~LesORuqbNh4{VAia<&GJ!B)Kya$wj@jG5;h|3r!G zh>XY$ZAjOwMK;0TOx4|Fe&f%$#|cQaySDV)a`s05^kAtB=e(oW#M7XfH0?+-&P8k7 zp13Uela+KaEI>yU*F?E_^pRO(zS5s2B2&SwjBW4b41zHQx zY7%#zEbevb&>Ivw`RYj?Lxy7ux`Dt=S4@0f<{?zG*=*MbuJ08w#0EYag*7t;>pMcB zoZ#Wmd=nSER9aNo2T^ems00uvntH+wxA851HAkJX+VFc7Cx!O?yPY^uA)D0H6|3ZV zpQoYpev6wGFrmYv#4lcshVsqFH3Q?i8R~SEN~;#~y>;0Sr#;)Tlz5pq=!#G5ID(`}dHt=qx~J<- zse-qJfU1t&BT(C}J(pCIQP7Jwe@_~bE>m}WW^Um>-M8E9%lLcpWjHAuvR9gh%(ru0S8Zsk*Fl5J8^HN zkzV&qOK47~%LBonlfHToaWvJRI$f$9#zVtvC;5I4O=s0UT_80J0HQZyj{nOtlzo~M z?np2{VaK8fleq_oE}oFMJS>$@%WNhE5IROJN*?W7-xKs3!bJd1tA4$zbU!|N1of}~ zQb8Xx!;XTh(io+Xck}%`BaszpwV3 zsEK?r0ZuMqVW5ME(hm7+J8U=z>;uZ%LcA<_@@tj8<-?@t^Us^XWAW%*Jo%BjA~wyt z`2(4H2}`NF(9mO3&zqdKe?Z`K!ES~V8fDsY&}}vYRxY-^mv%rthiJIOQO|d24e&u| zNfs;|3T(oLgmL?L;p`8O42tt!j>RG|>h$q!Phlz7$)?RS(oMrp#|oSH#I3EYX4oF; zAzdmSP`zup3PM(b%p3{8Q{1^;Oeb>n?Ux4Z~?mi3fhD~uBptfl+L z()W=s{y^ILq`rlN&6>qW#hbFTil zu!SDySJ0h0P4Qu~X@u{pvBKEBk*;~bb!v0|&BfCb)(6}>nlbZ}=Pah#2%W(|9M`2e zPQ{%z{26dC?jk(hp^XbzUozob?t9yutOIDk36w>mgZ3cWBxO%@2qP=AL%j>UHg7y?+(BW;kuML$VjOrZWJe3*~?7po5?5s`rWRe;nJ{LKW|V~HQ^&G zmy`!>;q2Zx>vyrw8X@I~@&u#05h6{!F&%C`xD>GGoz5cOrN2Bc>;dJOTMWr5` zRH)anrdXHf(nh~6fSFE*f6i3waZ(le)sz!hl3XOx=*Kut?@s)|diG@2`xMBdyI89C z1iK8i)EDXS)x%18`OE!+2@mfgdXmYGs4dYVcyb*Iv(n_|LdeGi&PA;m=>_G*l7*A~ zka;G|v8*N9Zj{7V{>;ER2ITxVuY}AvJwQ|f=BD4@ zuC~%p*&*~2w0#3hbe-D1Ep2}=Yd6)XePqp2(FZXGJ7S3!E z9a7YVLGOkbGs(yFh(F8CQa{844aQGB^_cwEOw}djc$1nOuCnd;&TR&UJ%l&6LY7!n zhfH{Ky0S~lx#0l(7KR(BoJRg8_ZS13EDDC2{=hXz(56-UYUoDPYnaOjSW~3Q+X#uI zdFC$*Q@OW1#b~e-u#`4x8bh^y@y&O&xOHD4_!-ozy!lvWBj(hUPKo(!h~h_K4T$7J zSq{YAxG^7D0!!`*7y&j^nep_DbF2vItI&Q?Vx=6_KRy&1(UWe^`h*Q7S3aZnIue7# zlu+4WlaRsaa*zP`Cm+lwZ%+H-17Rq3x|ufT$};27FByV$TNUDI!!$x4U=80)W#{|g zu$H94U7>|zYOs!&6Y~(0`LtU+l!Kc?xwi?c2xh-sSICka`>Vfmqh(mHky(w8^WFBO zl!jr7^0fDJ>+?I93IdV|2 zlQ`MYpXp#u#cYEnMSSsVGak6>?j~c@o_TB;;xKp`SKOhFl`Tw4E}xy(%oiZ4^N}ed-O->cCN9H zaXTTS9~(XUr!_Yc;$~4b>46^hgKXy%)5A5-dR5fDpBoW zxD0IBM>-wM6wnF&7r*HL^QRJVSEfNaepd2x^vi} zkMnO9++*_Kmz?e@?{eq8_8)EU#gNIWFp$Q9{J))vbZhX~4EyFTk5gNnRd;;y;Ok{L zo#1==&R6ka6PshDNFT&zS-NwxN^&c&m0h2C(0KAq{v%85vJ9yQkFdF@)#Qp zi#&X|G-KY((}U{nSHx1>EDCh>q*XOPXzla)_m$EICYaYYXG;?)MX8^7?`q(L3XloA zKVkyu>1oXy97rGk#U6PZ3MTC9r}FU5P7MGcBUi*nP8CS)T@o85Bi-<`efsU_<8L13 zS)Dxc6txuNFUoA*+v4zL+o;kYyY2@4`-D+*3%|UjVGxO8Ok#p8WttBe7Xn&JBN;-d zOxeyRyCzSz@G;n&;RSp8w}0BV{r0|X*jPqd@&6lOzdRKHf}`$JTWXe#ow=+hYQNv7 z!0f1u(iu6Z#$?8h*T&5-mZ%?rxnpZknmsMtG*2DUK!DRO;vQEeMSdo(`{UwXHs&aY zg80Ps^Y_;2`DB7E_=^C0OsJ#2RS(;ckUVrP3d{|icOpXEk2_^p(O1s=zM=-l3Hb2L z0|j_NM7?`3f=|r_TAs9*fVtTK1ek=o96qp#H(l2i8|x5)w?k%l^(c|=cQJ_qNnFjH zzrT&Z6G=1deoF2D?~m* zGQ12~@|et#kkWo+$StFljo2jwy8lb#;Wvx)3FiYlr0smd)UdM?^|9!YG zEhA4RMxPU8EC@tj;s%EJlvNjE`@4-W71undCmhpXh*f3x)CW2i>~pf({B{2iLeV22 zgU^GfT;J5~`r>T+hh)@O4qK03s1Ukp*S0L4N9FaFnoiNcRL%5`PG!-?vRj~HS=)3- zm_O#7H=rgU6rW-<$l9wwezV-|M0CYkfAj89o(uxq)}GoO7UFX+7JDor|Mq^TTihLH zy);ajr>HaPH`t5ER~)9pVbP)T+ab{qduR(nNYYP7ZW--H<28q|g8-6@pyOv^vPG7F z8HI=ZnOC_=;Cm}fI84d4`?AmI)d96`D-Y`V@z&6uf86Zb33SdJDF=yH@xW#uK>lzA zM>5X;m`vzJQeRGFow6n16=iyM`0a}Gn!!UuEg{ETiFf^$bohz-G4P2nTH>J}(f2}u z#PDZWq*+86q)>*h>!miiIIaPBs_xm2(rfmP4Kq(!RXoVZl(A+&mmJ3!WVwCv2%rY**ImLn_<+>tkD_v0v%Rb)>CEQ^uDZwSf~8;{=m}>$bt81*jopeMvcza7>ieqB3%D5gJ1NL`@?5w zVbcW*RYleJyjF6%Z>_YTy12a;P0<*McR^nUX9tiQYb>Yf!B*YV(iNkKM^o`jF$ zNZO8Sy`S|0xbMb7HXMC+prX=Mvg*E)!!2zZFuCfu-PaL+QuvSFf%z}EkE~EbtCtUM zm!i0++g?ad+0D(X*?$x*mo`S~^7kDi&E3^Ty}iwHhf_9}9C_&#j)n;uT$N62OC9cO z`T4m1-@`GH5S;IHDN2%mn*7AOUW%!ZN?RZ282vGuAXk{dbHphQtqxI!ySxN36AAsa zGWP(upAECUN?j_p3!|orQB#C(Xi{(z$gKGnx>S6A;5p?hIiVAcojY;3wz4w`K2Gb{ zdqNmAP2vlf#1y!ch6uG4w`ShIe8=_8EX=c0iDZeWwg9duy(Ew^`|(fcY+U&qv#Q;$ zhuGaLE=7sR_r2zD6)y(y_)*`DPc+7bV5wB>IsAK}LWpfjl86LSI*zFP3&ZMi4rEI5 zF@<7jj*Fe%=lLkyhnIzI>~cbWDsL9QOojay38QfblPh>{3-$mVB?n z<*BUQ)LM@_?_^gLO;5lcS=CD>&d$KsnJ_HeJ{r$pV>M_Hps@rvn2W66qW_qO?dc6g z<6r$2O&@ebyn=XhS1k?ib#YiqQ5v6=E*=!L(7ebsN{KW^hTrvSm*WnUnvE#1fRP#%?4#25T$Jl=lH6M+Q-A ztS#pksm)rvX^Wwd&_wEz{z**Pt?+IBVS_Ba%)_Lp9)UzrvcE2CT$Jh{i8%MjA9=WG zMON>;Oz1aXykDW$On1eXKLkBU+u40BJ;@zS%ZQF=M!vpy{$4P$eZ_-~@`Pug=zTB# zs$S!U=}LmuC=L;*w))F|IN)YNZNa-%&!emHA4gO#+Ns?lYh{~Wht*~8;#1M3KN(K| z$q8xy+d_Lu1alpq%gzS(7c*hSlrE;_Kg?@69u%9s@852ZhOfp>Lv8M$^g=}0wL4X) z_vZ~wc;ialO_E~1jY6}IZY}?N8W`*cB$*+IBb*y?|AB(htV4b%w>G zcY50VmmGn4&(4F!Jgp8AUXxRGRmsm_&+ftbQqjSLw;kqjVV@FjjSljQu-Sw7cTbcX zP<4tHfUOfbRL(c2_rVsv>u9vw3RHCAo)AmvfaLn4Xf_+~_mhv^Q*EtyJDb}mSi-~H zA1HzKb$Pb`@TK=B?dQKZ?9Z>xUQbNp&u#sz(K~c*-DDmtP^Od1r)XeZR%DF<#3fyE zGFruyO`>A51tDbQW?pPMCg0%QBFKxTUF9k^0@4McH8B(Y>b+l}3WKd8`|ZITKmZWY zJ*C`v1is+-6aQy-M$?cg^j}Og;9XZ=p zPgrNl6FvL9gz7tIrN;V}?fg{WFa98&4HynfI;k#U{)^x4f#W$18Bd^{jQ;+K1`fe3 zBhS?>f%SyoHWMV8qK3%**1iZ;tKk_6QLadUL?<3|Vj52K7x8~9=k}2M6FbaK*d;_( z$&E|x*>`patRye&JkUM6?d zyj#+_6H5Xu2|#NSZMIf2LE4I_+<~gc#va-yji_x-oJbXRymW zIV2Y*R?`CM;)i-a*D? z8)m*N59{WVy}#u(QU^_0xJ+}U zus(M7iCYM-<^mjwl~}IvN%^xEwZ zP&={TfA;m1Sv+ha^~;-eK9K7Y=Y)cQ?0V=~W8c2CFC3gCc#@N&VsWvoyEomJasEJ0*n_jkV?+`Htb6++zB9YLxo%PAPed z{ww^1!uDY4NIp;RRUdZFS^<$mu?Xp)*nORfVDgu;IPRK#0zZjeltf~+6#0_onNn`d zO}+J>Ncq{Tr~2f3Qa=7rhz8fMhQHIEd~&||^k>5~gg+eOWIH-lT+7yH&rVhV!TV;1 z`ibCV-JB!Int6;k7LV-hbtyy}4)G}s=wdl`RHtkJX4bu8*imaoT`_ta&>tcV%6r(k zuim~^^Ud_>$+d^n!szvAhUIix$v8EwzgKhisdbo)o}VG(-jfBAJek$qdUGDT=^|&} zCg5dFmf5&It&*L7W=iiDzNXf!wcYZD(@Tt-yb^n}edjqs>S+h=EUHWZl9Tbv_VGhT zVY>r3l(%IcT^xT&4vck0NrsqcKMz7BgJZ6-sOzRgr|BBche$aTpM|Vq~PZ2gfD9uI-8zPshdE|U`(~hO& zpbpG1ZYd$*lBnmWRkwU_A7t%m_D}<~z?0B0ULxTyelg#fWF5-{tMPdH>dQg4d)SDy zy^yTRXa4^g&fD9c4gV5&7xsTatep$PpWVB%Z&meX&wY;d65DmI!Nh6GNT$a*jMmt1 z0ZdG}26PkQ8ktKv+ym-62V`S$ZNsU}Z`QYLwbC#~v!FZTw+&t!~DPJ*BTtD#)X4^cfE zxbkmsMUMlXXw|2KF*g>uhZFnM&9B5LtUOY9HLooipN;Tz6!T}rWQuoK^a)cG3}J$@ zhcU4p3`~v*y)KA_$wlKoeboMK!L(3E&8jZKAxS>$=Cc((+m0~ki|c*QCkum`L1`TG zul4W3_*ZcV!`hD(HG#IQ=+w+Z!9bw<`fcAAja{INp21|6ngbn5>{A}q$;}U){r2iV zt7Bz{1PyQ^zp;9z6^L;2!H;}^b)sMP-;HnMUfowJ_K*oO7LP2;XTxh3j`re@>pmkS zPA7SH_CjzzDe!&hm-9kX+!Qmb&>VETu?o<(0=>yiZMw>ZUjO)=L&t_l1m=VX;lJH# zqZ8}2`usTTM?oeDyIT6I8#`~cM7R#n6FTrqTcWk#WGEg3((qH9OJcJrh40M3_*}^uO6`tJ6sr|wFcR&Fa9~O^+W>*rENBH7>RVf87gHMpY6mEum&~_n zAvqu4F&!0N{_HtT_4DykpLCaLyNZ9Qefm2O=;&@5$hY#<<*N#4Q<3 zkwu1h%OJ5-n&k69oMGGxbS@jTRk-j;`=IYrBVDoS(f@de^tUI!A6^7#XO8AE{(nrj zv2VD}<+n5^(b97*Uyl^l^s*lJge(b7MKYE_&yjvwo=!h3EIVNRML^sBTANEV=(_e4 zN*AATiRnBRRRpMRUeMPl3~MV2ef@HN_wJMCLPXFy`hP|J_kr9D@|W#u>Cop-VwDxJ znMiTl+|Kue3RsB2fu)d*b6CLmU=?!3riQLy&9Uj7Y&6@!8NQ6JrVoE-^G~h0&Na^S z&7ZorazJEZhbd3Q{05gQ7rU)(LVJdLiUZm0j9+b+!Nv=lQo6vWNSt1;yM0&$pUc_& zwVZAj2PmjdMh_oU5P?h0&hM>5b<$RG>~T-pm2a&*P8e~I_9+I91xoi=kpX@IK&|sBtI8Zh zsAS-7W)`@OV!IcA4kdK%5pAz8{J-c-m?;u>iqB11(N^{Y{n?h;C$514q%6E1mbT^;MF7$2oHk1H(^%1`$e9NybLIYaL z_AVDLub#gDv<=j@gb+%R4Xo5-fjy9oVe$y6&sdbKr%;FwwD7TGSjHtlEH6=h34*>i z)VCiCi0-NHYHX58Cp>4UEEpT&4{wov^J)?gHUrO*|V z%(v4&yEL<@6t!wRG^eyQOul5)4eMfPF89tz;avl97Jfvdln*Fxhk&E1eR%zHHT%mx zbgTZ^M_lyYb6mooA77^fyQv??6EiEp5LKogP(`E1&mlo+rn9Tz;y-n?n4!LH?7W!~ zbAx+n5?9T{7FHF6f?&y`3xpEfd-tiI*~+k;jw{)Nx&`NrpK z!^8KMVn`WE1+fPX;dS*vxSRcp{y)Omb5;Z#iF(RUUp(ICgdEshBj)WD3lI}cW?`6x zvp1DRMowNpvV=Clp|_}|*LDxL<77h;#q-Mtkd{Vaxyk}l+epBC<+lhZUZSHMz7n`XL;ME>UY zJ)t2KOEOwCwDMrs5ZK%}Yt;h^9mqNQCGxc>iv0loUcEB`}Y1mXX4DayBb9~d?)m& zhHE!_dZ9oOoN~Ag?Ts2)h{bawq_LpFqBgSVTKMXjK!JxsbsHxNw{*M(#Ci#DKt>3b zv&-{;P6y2zpuoZAwjWEwP36v53#!1*rT582jr*$7Ik=k=qABI%?r!Y<L3B8Yc2vtu~_K&xs7TUYM)W&DGHBU;D!yDPC>P!@ntPxB!)ETBTS-6-uQBCf&0exe@s3N=kbyzc3=8UOs^-N$nMBJy;d@5`$XS7 z=A^9&|B?)9eEraQrG0u9{nstVAt-{aEbD8{RufyE)NvF~zMz12SScnG&;Uq4x4*D! z&fhD>!DJf(D{FOtLH1}J!G>N$XI+qk3G=N00L9xMfN2cupN;Du+Pg-jprXRzzFdQ@ zYl@Hy(J^h+=761zO;vvRlS)HB_LtnT+x_6X{;n5^BYNME2!LTn-MJYBa3KtXStuzu zwhPBN)lcgufA`^12%x;9$!7+G9=4HJY;7q=WPOce-lj z>ht=KtXpX_*3P$Wv=pYhc6zP&+wf5@gtC~K<{3U?-p`1q!pA5*T*+DZv-X#%0B!k6 z^IvNNuyLOxZAw)i)_dTbjp&l~Clmd**=OwPr{>8RhYw^D0HeHzPL~@%kU=};)2xr= zzWOz7vHk)m907Ti;*y@%hgX0`yQ{3)9pS2v1ku3BD#FNmG?^VS+*cZPsf+pC93A+y zl~Lp3;wv)lO$s-fCh8MCCirC#~fBby#7 zNjRUV-+k+T{C{^~r67o~m#9&Xw4gU0tZo8){p>riMdE;1-+>8AX=@KoNhXam39c*P zd;BZft7qRm^vYxRl|W&FCWj2Y^2>>BI@fcaL20C=LkXzx>2=xk*1@A^y|+3o!?ytZ zQ*4EQaV;5uFl99ysk1}%sG{VgfC#bg^#K$U9T@uP31x1$!UgR#?1e-iwNK0Vm(HiX zsJ{7gmcgf>D1hEW36H@_YH1X44wV6WL#>#Jl-^z4KFc;cxaI!K-ou_PWJ_mbFs?qA zuZ#D|KDU>DVQU^D!vn&zxm>55VIYvr7pq3tsjZjt=&OZTF4OVBWHnUqh9L~K-sx9L+-Cz`C-yBrW6p;cIS zH)NkcNx7lbYL+4$cJA|gS|{$Eq4VnVrTOdcScznCwarFc!vViK5&!s}i320(=;Sll z+;MDvl$npiD;sVO|0SH4bJ|=$4CHQg{?%_)C+h?6dc?L%K&qeYAR6uF6QlWzMeNI4 ztV_bcP-nmPcircof{Ush25A|ut!pE&ZPI%szZ&!`15l?r?Yfv`%!P8pzMGWUp*>7a z*85!t5=A`N8#i<|oea>E`x7J*M!>42qz6ua2j3+T5i%H?ZN~mDJ`HUD(+|$JQu(&= z9@48HAQqeK7JJ{f`S6@x(xaE335!i9+4}rV^v&<2Xt3K~ozA5D)pMI*!J0b``+f*f ztSmiZ>yJXU9pTrh-ahWNV{tzb!!d}QPtwA5C|HJh%A6V+5 z_X`-^!eb+5?#(*s&453o@cJ^Xnt7s*P znvU5HG^o+%^evKw$yHXb@}Vg$II5chO1BDoy=NgX6V>E(_qbNA^7pPAt5tt}EG03G zN<@hc6+6)9=2yRy_5*96)ypsQTXT_w;$00W|``V`C31?qk{A;zi|1I4O zvk}HgZzs}P7;*a_JXW4zoHdGJtb7<6r9VcpO90`@tG5r*`~bENUc&xt4A-D9}uRA=(<7( z9NEMI@ZcMZZ|C+P7_3?A3cGE-Z(vb5S^` z%hK3F*rd#qZJ|vDIsR~@I6)*9rg8b^c>8)sNMU^r#Jc)%yJ=>>jeb_Fl$@_ywtYo6 zPDjHWe>8NGz#UR%fn{|CgSrZ>r_+}p8S5vQZ<08gHAtpPCP6E2yuZy$CcsW2Ud=aY z9;Vb}n&L@wnf!x|&(93foF8KIZ%WG}2v!x$aFawI5g47Ft+#l$lauVlWQd(D6H1Y* zi0#Mfj*cEEO>&0C= z6A!^f*;!iq-!vc91%>%{shMK=PEA9WUtHa9i5bE_^sCNXmaJ?Q%}CwK)1pjk#C~^3 zF=$l{d9P<;b)_xOjLKU2=f_97CruoR)uMeOZJaZ4v=C_t#pHGG_+zll2^#fCBSiC4 zdcJ}=L~{&fZ(zHon4n)m0Hq*VPuntkcc4G5<>0+IoN86?{9OD*YSwpq%PE6eFurUz z7#>K&v+{nYhQxm9>pJ{fx=*|G)*t&n#iss4Z9`Hu!vZN(S3eO%x4W6PeoQaVbFH~rKYm{9PbUBjt%F;?S=doQQ0`31gu1N#J)ggLske{^2XNmV@5$5pvW~eQJc_2sTah$O!3jB%s|jjBXO9dU`GYO zG5{J0lx(-Ismb{0nTeFk=Gvz%YVkqN{7vuYLQ@Wh^>A(Q9gCCEbK{@i?wI#WH!)2 z!u4x1ii)JYom@+6takv&k|}AQHmzSYD(@R`eNx1 zG=Q%QYbf2gAKRhV#EUuq{(8v#$$DWHcy@ufbTF@}%n z|B>y~vmo(!b^bAt7AY!0@G#CU7Jk_i6>5i)B4G}HIB*`+)N$Sz>DA5SN*bymRZC^q zlgop4^5SgJ$%6@{4!amKV@M;Q$^nWl@F@%G%pljX;C(lHo}K37tBEi8IhB^S_8}FP z18sZI6+#0|rtHPxuI-cdizbBS{njO<+7BBTFM<)GE%B>W;yL#;3 zf>|eY#duw;?dr(MG6O8dtHzW=W?!BIo?nytukH7jYPRMz9R?JaG9CRKuGzM1TNB@1E;K(54Im}twg?AUUk`?dIFTN8my$w`;<7CK+KvI6U zKbG?RXe~!Jk=Ro(^3f5pm7UG%Ula$930&&?>U#YD-k%&{_WVZ5{=ee63ymDeYW4l& zq@t?*^GxFfDIuUI_s*|NWGFh*Fd3s-W0%M;SzR;$ZUA ziKv-QR;7eh?fct5WHJrE)tOxTnpum<6d9wQcNb zJe~*j(%mzPmOjxutGj0gnsfC}puSGyxa+;pmiC9e7Y^v!=Ro^FM3u2*X=k4?cW{lb zE$>vhSN5s$zDnp~)DJ7QsyK?Y1_sJJUOcy*QyWKiJA-_3#lQ$Dciv`#1OADYC;;t2 zsoEi?b{#AxRT%~=hI;UcQZwRDIc0&Ah{M5p#tbo(0#6v!>8ZYLaIMZxasMQL(Z}s` z&jfr_bQ=5`*%+C3JAl*1@F)GnnKJ(6C>H@O7GwIYK|wJhVGr?*B8>J5bbq@E9B6M} z+GxY0VTqfLjn3QX&j;{tuGGPFiV{?m;S1)Os#+}*{vPj6qm*3l^Pxu$P1g-6^aw** zQOp)oKq@wrXe%?;Q4E-602w{IY=db}a&m{cPCMKNROEU^{b`h&|Dz#iL?^(YwNdpF?C_6kO85bY9ze|F`eFi7!QdU*<%WZnKaeL0$2>vY_ zNp5z*p*JueWFqj#Hg;-(CL9sx6E%KYL)M2JMJ|&a6X!>;h#0PqgaGVx!iun67 zfbR_ZQF353NE0`TM4s)fp6%ehh;m?D3phBpqrS5`n#0rpFviVezDyHKGOs^9alK$e2Wtf$wfI!T z9M$b_rgfCEYN)+J11X_)85z{#<1^Yx{?1tI1SJRd$yVfZ>(ztgdDDL101TFZ_2V)| znGA`VEEH;`EP22JeaRRs5jMQQsVC;rfN_i>v605RImputIAYSbqp30tp-?O7w(R<^Le=gia2UJo6MN>-Vj&++~;7E+7zlWY@GjQhFWAY?UV_`+M> z`bTPD%k4?-%htY}Xo^4&^s~6w3DUabj7i?Q*p`k~4dlmY@=Bl-21!0Ak_qEGG(D{! zF|?&9N9>uoM6s18tOnmJa%J`IAc76~=l|S?FQMW+A~&XGY!&|X)$v}wr7<- z0kFdw&_Y*7&yUxp{+vopy)=eQoCV(0EG0U}40{m{vY*3U8hCWPJBCC3!t#b?pW#Rw z$K?MEXyd>AcJP}q0o~xB-@K%}KJ3r8zo%8lRw38jxANg(GkSX_SWFMZ3ifBtBjfQ| zyx0Ggb5xb-@BYKQ9gY2F_uanWWrhEx36#P0+@bcyr=ZlD)BZ&By?+pLDG&-LqBJ`{ zZW;<`LwI*N2*I4;Bk8crJ_!}&A4*vFKlK;g-ocZ34C1a19A8|Fa0*@BSQ$rqY~{ng z^Yy>Y-Ysi;+J>$A0z8oV4AeV9bJa!sa8hWnQ;NW0tttCNCP)so!EcL;DBk6hF9&+l z{}M)FY;}*ssXeK03!f?JG>JN?Su+o!j`eQJ$|dJ@3{L^~pvYvCzy9(OGB?~~vWVbC zp$`C%jH5>pp$J>!<820ylUp?A4{G~$wwGBEFp2=F^%yYONnzR@lE-qT*^PI)+u$_F z+vceMy`@atq7-|k?^VPFWV`kDKhXkbg{x;(nnGlPw>Q=+mj(@eXjeDPGY2FwC4w+i zcsr3;;5Tb2y^~U6jFDK_c}fa19hG_K+!v$fv9aV-esi_31iKi#?OC4^c){ZJ=BZqK zr(JlXZnxC5(;v{!%KG^TAQM)@f96PPSbT@})x?h8?Mc+kUW{yY^2Hgmfd=AasmD(V z;A6Se>u72T#e-CR=@=)kG0hX`hEDnmrWIy!&^)_6_;ZU6v%huue;24(D%f>qTQ*RL*jwC3l$#M8nj0gQ`^NBfMP zY9LRfmE-jVS4iVIh{je@^8y#H7Z8S35vL~ z|BaMoj;WD};&x_0gt7Ys%f)Sd)kQldN6NJ3tlAK4<&cwQUo2Wg_M65(-+R6&TRBaL z2lf2u(fqhW<<|LGQzRea>8l&1epIF=#4pxSqu#0$ zMx69E7#@_brq;bMyFBw)tD57t7c%|JKmEu5`9DJ{SUtt9Jh&X4EASF+y?#K@1$F)M z+>wuM1$rCUyM|I)Cu7Sai58#g`Mvof1s_;qNorco%Si5PQc`gIdp9&cMH`bVYy8$+yRg ziQ=S70g~QPCPw=MUust(ZbS%_x3q1@@FFF4W_C@+j}lgG&}uGscqf#q!wS&K|KU>m zL(*UL<25w!&4K=8-bnLHLK?v@so&=%%v9CAXtqkPwkp}MGYY$1?)LtE zD!9AlVOa!VrvpmcnEKSX?xCSJV{yOMX*PcIyXH1RH+Be$X(97ACEcVm(I*09xRX-2 zQvS2_{NKS|+*_S8VAe?JE?miHu9>xKd&wPSYj>2Ru%Yd{#zyrz?fN!WNv(PJXVR;I zl$SD8wH;`BH=<*eyKG4?720yPqz7&KNaIcl-7bKuUfXbZ8zYL%%lhd`6SH0UOzBFR~U5W#J}`jjm4SP zFUX&n!!E**ciIo9>*Dw#T~{=mBS&oyO2Q_LTgIv8fuS#W={$E%FY^eblh{NoW|D85 zqPQvULhbqh9xJeK(qKcz;*{ipu2c@P?_B<_M|#}eQ$ztIXe9EpiP$%MB_2SFs=c{rto_?cl6CvkdSWeKIio<<;EDSai zz;@3Yt8C8XM1M7HvJLQDy8%;(4HKW42TGs~BvCq^DW^gEi%RP7*_o*1*H`?fYB_WM{IiqPvamt$8_<+&hIi->lus@BT824s$Ta z+_6q)^oTO{LZYNvg?%6wZvgbkCY~Adl?=mvEf-HTH#T!swcSH{(~7I}-~MA$4`$PC z0^hg-ph-bDDdOjar{$Tz4+k=@3V{)QWOeH7m5C#)PnMI!#kZHR@>m2^5-a_f(#~)74q%4GrNTB>%*Zs^ z+*m%(d>v)!urDSw=Vv5{TF1%k7yUd51Nu5aXQB5|oeylKuk3+FyaEDLcDfe15ZLi3 z68`Q}LXVt_jUL12ZfV#OhRR8&8ZMzz8hdf~Lpso3E^@+=XH26+E!guxCIm1AH_JX^ z-k0w6zkg*+#JuwU*!i)y;DO38m|Ddyy%+)GgBi}BC7vYu%d)8$|f(Pm%mE) zv>0_MfeSvX>RyO!*CgoBM)kPC zKYFIT(-+DvQgxeUs1@}rN=nwbSPY4ol(GEEx_g`@=aaaM95?6LE;;nVq9ZS8Mf@@f zr`|H4-oWNUv=18w27EqvNwwKQ1N^Rz%BK@!L(p9p!jV}3R)orKNBWf*oLucFTnwHn z+#%4p`&o#`e4{$6we}L}8G#2zWfh}x`U+qcz0=-Q2+`H?`9AA4ieAPRNo<<{W}dio zyJ17fNamxHzKCKs+%Y_ULNPJynMy)l!^{Iihu1GagM%P&Y7=%82Gf?DI-Vm~cXWSJ zHCF8jT?V>&>Z%!v>h^yShS@_`-e%^lqbTz?N$P~i z!4m%$_k3_4ex}R{_#i?jZ|LWoI0Ft!LoxE-I>jgXW}{$*z=+Ff38bLfp9 z2EB3r`m`dWFi4d>cSEpjAR5bJePRJx=@)~OaalifLoy)TaFt{qpBc=uF-mc2&ER?! zhx=`!Ipk`ZJ=BYOl$r_cjhVft$PU{~bpUgqn&T0mmeDbiQuo!T zLXlH7agJc7Q#@m=9Q&M`tOSrKrU;`xPZQf^G@YDPuzn&^TBAG~Vx2Bl&0e={*`e;3 z_Vak)Z=}&qM8+2y#LTw;=%}`jV|{j;6z;l zSwbGcSz|EqNbt&jS0u&6lp)cz-T3?;&@|pl%@ezfVmw|KA8dWGTc?yBOuKc50Vhp6 zu6aSexVHXF*yF9PWDI6VF?MVo=dT`%Q(_W%2)hj0fDmU==ne&%B1e)(^)=whl*lUf zO(y;p|HsCczZne%H%iah8@o6=e{nwY7~uBuj6Y4wVw|&P=O~`oJ7~$t@2no}`RU;B zUS6E*TRvNbF@q#qoXSsoo5ISmw?T}ZyTk`Fr0)5GzwqO=oCKm*P4@mMM=vTVu$-S& z+ThrAWv-^>*r)S`jkm~*O-(>0_x(ZrreJ8IigLymI%1$5S{S&yJJ_D~pv z+HROTz~8hgb8$h@xF9jik5!PO*V$WoMs16|{^2z2gm?k#m(CvVDFjSR+4wKN6@ysk z&e;{o^_4{r-<;b5+~(L?o~DOV8VODSY+kF6aa12=`rgv?2xEJdm!onERERp-10BWbb$x!1s2|ElAO9X$ z7-@L_F`jK}@9e3}pu6;;S-&4$FZ04HkKq+x*JF8ekt8GiNR&R_WegyHD!-NnxVoj4 z*ARU>?P^a;GyBYrwobbaJC^Zb70CM$e|r929qWY9gFfhJvM)gwz20v82i0KEjec#x zYP@O=S|hlhYp~pf+nuMSojlo=Br)*H-63Pd4rgQme($hW9qn&__>=nOE@HzYsFUPj z_r+6>?!Uj}M8jJyjie##M`*_bi)3O&t*CxZ;r4CY~J@yo_$x z%Mb11^*wk2&+@kd9Tf(4S|TG6|n7GkEDi9d~pC z(?qVk_KVTDs-AA{@D)fq_e(wgG33aue}Fs4aV|uf6n!T?CWrCc&apRYA~1*KbTb?X zRMj#=^sU0jw#9?qPFgXp!3QZ<+a#6$o>j_bq<0=pWjoc*g$;*SqZyJ zvM}Ij@FZlX{4vN^89G+~beLU0p>R*jlTIyqr4AZlPHNv^YU;B%ZG_V>>y6dv6FhW+ z9gIpjbP8bq=wrwHrp*2a^RsDv@A>^thm)$T655{H=$NT|fu8Z`V1f=gPh>5MP_!7; z>7cPMpp}V;weV+y!Zo?d z7Qt<)m_Qzqa;IqY=VgW1IctbU2^OHKlvLd(+Q6i`#yXXcLQM^~CR@qxTsZnOeMWi~ zpW#uTACEO5U++13`W%d5JK5l2KXXmck1E)K9v5JaySM-%sQd$$@!LG@5JsMk3~YoHR}5DX?&^YzmyhL z^k-N>Jvg+WGkUxepWvnP&9j(#Kr5hUL35cPeyTK=f^CN-evbj!eAR9Z7ENcDg2WkM zeE>$LMb@1?V+ycO)59{R%f2?1L(Bqeh<0Hq41^=RvabG+KsB2dP|1fk!n;NREOx3J z>&vm=pjcaPS{pCO5lybriEw|}4ec4BwB>*Vl2D1;N6c-|~wu4y+$;4);wp z)VD`MShD7QRy&h`SZO+`(;75ooMzOXnd1_Yxv(qag$KL6n zc+c6AfUwRtLaOoccw8TVcJcsnv+U>%4yqTgZI7A-A;2AVF$pFMOQNG!54+hinsht2 zJ4kokjUh5d<6{a7SlE(x!y`G7A8m$)w9&;-X(#1<(VlcT*}2c>j9H67-K79jtS)oN zm%^5M20F~G{94+WFSc-_fC;$hKVc`!Unf-=;_kjabiX7v$b+7kV}pj|Xbxv5@h}`jN9awB6L5Mu z55vsFL2KBX!W`HB@uA(<{ppFuE}Y#`&K|e*Kt8`k2JzP3GL!Rc+kR-s7IRRUk7ww>9uzH{Nx5v>;N#$3swO60{FzVsj zVkG%|NRf>cMX%HZF3<_V-nj0WB=<$ACo6I6;_F~;3Bbg5F!KEX45c8=B2mezYKA71+0#PDQ_ zwhr14$s^<`(AvK z73IrG(VU9n*?vJ~pL~_n_Z)>xN*eafUZ_%Rxd0jMh2v;>^V zK!;AJ&9t8>R;zuE0X-6t>UKT=5Grh2w%mogmlt1M!HtUe)a?vcNciVSA0vd8HF($t z%QF^)@)EOx7B}de;F7Zd&dBIvuQ?{|z=zUS$xLAKQO!TsQ8pHcKgbMq;U^Hy= zRv#X8?n;mjbO=+sGq60LDGU?mrxS06jBoOnFN>94vBQ;h?`!koxZ~3W-TXQTPW;7b@THHL&(Y_ z`_1ENrY=L}+v}0A#O#-5Jf_WhYRA3#1M4T>19Evj);Bu>-JaPr$L^G-#MY<*2hIV+ zT;&W09S$wVu27w<1icAEI`;z0FqSEH2b0zqaEk|gr*hBvB9yWpfX zJp0`7FNMwmume}J^aDPW&9rqE)W!q9Kb-FQ z5%#zcE7{{>b~CQ>pLa7WCk08OvB^8Fop(&+Y)UB6i0moRa>=ru$r}oi@|<-<$n5-Ksis23~2;#)V8!E~;~iY5O=T;BS}zW&-0jD!8iP`Q<- z_ub@XtD$~uR#94U?PQ#QqZO5n%v+xj<}1PSuxCX6CPs%Eu}|`>{pFC;H`3nOS0!j0 zcN9AH85J=Kf|)?C*4?9^Q;I&7Zl+Z<$0%E*oQXxfeZJiicd^Gwx|&lvz5Yx>vTsi3 z3e{tyHu48Ee;DQ8a~iWoZ?EzT4!cPCvxzu;scsp!Kpu$EiT!@s7 zW7l7CX{j~ZuRthMjsa3LoI=+MoNmpE2ncvj^fJEpv|hG0C`qEDWKGKFGLmDgITaO* z;g0gOUtXebCgnQ*a%+7O`%@(6;05U#$!sRKrF_&C`m@iVDg*U}?MPYl(s+n`+AUEh zxZ9^r>~dB{&*U;(4b)2g%T1o1mShrK*8&>YH+48J320A!8law+Fh*8^hw;R2Gtba& zd0O~a+8^sH;}>N1l5hQ(ztma}J|x;B!qs_fdV}Vi?pIU1aqn4`Qn`EPz3@+vP-(RQt^t2jiOlN zRKAd5+Jx+cT|ywuT>*P*(0t*s{dkrhylF1v%L%ms_}Zqd!4qBxg7q$f#P9XrBp;k; z19JsWiF;n2w;yx2+-tBY0sg0dc%-K(F2L=DcU-@LyW}VWSUpe_z!hmMw4Xq%4XcF8 z+(gnJk`js{O$<&0gGjdwVv)t8Eo6=?w@1ytCU?QGOi|L@aA{Dc42o*#4#vV_E<#c@ z$Q+tgCkvBjzBv{) z`z6bgSYqP_7AOh)mGc)_O>`a^yDaKmR`(7`5I*thl9U`l+=&P}%W4=EbKRf2bzz@k zK;wN_ZB?=DNi(V^O6mU!H$_+m=0$%GEmnnbZV14Ph&^5j&67S~2Lg$zo_{n*vO9;? zP#29d9(usyHTPy&#cYQGPckgB+2>CUcsI>l>FMMYs@(j`E^HU_%;*S1w}4*vWATRA z|Mm~j;AxX_3`}8UkGILXK6Mg__ZkWOS-k?ak`H=GzQ5GvK+E%Dp2BPjw~$&W%vvi3 z-DRZ9eoy`z>$zmG^`_c5++{YmnFsfh2jQ>!yZiWkOZ-uWG}1FHu4Uh>gNjO^aSVjd zIW(dv`xLUz+orJdkOU-L17trDS<+$j)=?<_OOL)&+pReWwq2%=4=r z?VFuWBBnV~dej_y_Pq*F5x1tLTF%>x2YJXe-#Q}cc-@WHJjONO6v+t@&vW9bc60#W zJ$INZe|98tc60`<-aqgBS7m~Kw140@b-t8eMToNhHSzkqw-gPL>Sm@Z-@Uf74LdX4 z!owiCv*x`tl!+cS!b1d}o^_pdT>)6*+y`=OG~KDw#o^Ra+K`pO#}gJ7Rhm`i*BufO z-m_f~1jMr$jB??@^Y#hVUp`6PPo7!DgkOi-R68I82Q6{P(7s(~2L`Qu z3cPjKJ0D$*&#eJ)>6TJKkQZ(CWHB=x90c)$Q z_Kn%ieuy2HOJjn>)%MTd9SmhYYSezu-||H7&KwSLLT5L*JDB2t#Kcl0_8f#se{y=b zifmD;wn-j`zmoWmA_jb7JOuGL=>P!`81@9FNbO0l!M^isA@WlFz~K^-}B zJpjB)I@bq9YimksZYXx7;lRp;wQ%wDes>|GJ>GQDD1J)2ERX`FPxp#sD>V=*aOg0Q ze%2c)t;aByCnQHFk(x(m%ZLxvCyq!7Uq!v{fpTp#ZSNWYJKoSR$k%ix-WMa;>h!jb zH~E(BtLe)0+~ zm@UPoK&k5TmKet6{uJ_n;VU<|-^phJrSgc(N=kHyZBap>unDJMWFLdtH=wA1wqy$W5-x}A9ASuj+*ZOtoU$bE z&@dZk_1FrD@0|spp6Y50MA#UG_9G9=Ufx`)iQ2FkBVUXPgar@pJp!xgf{$0O5 zD?Zfx#4^*8&poq@ zX)=?hNx{alU)|cidt(hb9~?fKd8FmKq!Nu7*gIgUG?!mWuk6v`~oA7&>8Z?>F<wb{G=w>Z3TkDoH2lWdiQI?gm$X zNr#7qC+1cw%W0VD;p`d5+6|v#9{LIg%P-mNIuQ2Z#N)c(X-0hQ{6JK9P$i)T<768D z^!PbYw)6K?Glx$m3*a4x@2M1WRJES zE(1{=E;Mzh-HGGj)rCLjxlEF#lqcF6sZ4r2a%e7zbX+UzW*mL?B+_(3qAZ3l&&_d} z;^O`gjna+r%}j{Yp%R|NO z59&^Bd*c(chDIegmu-`*E>6cown8W;M4ti_8s0c>I|hd{twhf(=+4(1x)0b&OtX-{ zP%#Ts#dWXwVOYW)C;#()AItt?#S|FR_gv2t0>FK4ld2b7KxlcI8 zN-Q5$O|vGAcnz*R-cWl49V_+Jk1lJC)E`o!l3SqRUb1`He2OM1+_Wapw81C)bm9JU z)0GwB-JW2>nLs4wAi5Db#`|Xd;%CNeP3Y%ps&COuxyO2Z)SFqkO4yb5n1xC{5e8PX8EH89aoTjP~UI<%ooQgcVjWY(O?-R$jAy*Gk`K=!DY zokkPyk@Jl?d^u`A{Z}z5fxIk^%C3Sh$h@oVy`63+KQUj>ovl+`z7^gBdK7jKzb?Ia zTO6d3ah9O8NRxH>+a4~~Z?fCe?2VF5VxaT6>9X1JfhJ9=Og3TL0M@=jZ&qAu;lYeL zSsRW%7nr>jql(m#a`(s*^2u08xDlqyIItqw=$ur%h33VuikX)~0;u$uyoxdx+D|Z9tTH?7<Pb)y{l<9Hyo)gsj*#&c^lkY?9@1v3SHx*YSzenKfidCeP9dZNW9hY#B*4 z^QB7Ggzb~y;Gt8GiSnfq@AnD=g6(sT#4DsM_Q<)8I0t&$&>EK+BS;L%ORA7Y&}n4C zR@_35UQ!c5g__Jslq2Ad@1B&odJa)Ztc2sAhHu{&R6UCrgzZ>?t{N_k*{BJB%GaVY z=xS2+48>)FO7xbrogPoXRaZzq2bJU8?Rd}ay@SyX(teai*=b6{(Ntn@t)xiux}*E* z{KWAmEYYkOLkh-QEH?z;dXfuGQNENLM85v9IGqSv`IGn{$X5$xDp76*YK3IGKj8KD zNope(%ioV6kCwYzVZ+er=B1!KYsk%Qs`~4?R48fEdHQm{9EgIq37%EZZ+|#}p8{Z3x|^w&Z+EeRw{SIDG_^=D5Fm0NAC_yJPO`}O3A7jB#mN_sS8ey1 zSu8J23zHQZq};%8#_$Dm%rx#+v{_-7ya0VCJTRU&rM3I&ceXn zm|||%O<#Tjduv8aC}a+8b~W2JY2Z{xP)52qej*PgB-|uzKGt&U#mvmr@cRU|`koPx z>h{LZtjK>2h3x^7+h%X}go@*>185YXG8n)=)7tzs+qEr^hanD_mVqXArG~|y)3wcC zgl8bjNg>Y_Q*qcAy)Mz>M3asLX*R?ON6LDcHgANx{6%*LPrwDumG$#ac?Hh3A;<#> z`f&MIhd+zLY!?*Gyv^@0CnPd1^V)Tn@91g{3wtHv#e@RW%IME6gqaO`(lEB1GRRXq z(pV&ALFEF0%$UNLsfXjrd6rP;cIEWYwR1ih-NGa>?89z^V~ZW6VwG~KfKQj_AoUun zD)*`xijNnPNK36&)~ague}JX7fR_TpZN2|Ypo4QjlbHeIy}^^x8;w2v|9oeCrKr4W z%O~mltEb~^E0Ru{a;-3eXDNTY++U>g26R21D^e92$TpheBW>5 zSK^tJuYMnx(x44hgQ_q#jB5OS?I_+?e-VIi}W|S9iT)$k3JqtV%<=vC)O=|hHwAN_sq7svaH9^d5gB^(5vX-l4ocs&F3%WGH%F~x)Z9b+pyX^c#G=1w2HfGXka(>oPuhNeal8xG{F4~ts@Z3y3A&!X9xPju zkc5kB)q2GB(U-Li*DtE}lEiK6({T!hK>@|D&5DbnBYWejZGTY*4-vw?)?@Hq zh`-j}H5`BHIXklIxL2Dmv`kWtaB^9Wrh@x&F6DR2d)aqjqt(*W{Pk(ssBz}bkIM=X zkn%{|Qkoqr)&_LNN(GdnO5M$nn1KPZmk|Uh3~SsRSw&%K6U&kJcOg?@-GnREU5(b^ zCy$~6s*hklv`d!8f!5^2C)c3&4XDcZTrvT)iZ66>NZjIVC&5T;3`S>D>2!hHS?CdA z3eu@fE#uXcg5!od7*Wx7K(xIf6)5a!4p;LPFZ%k1^yu{{dY8!mePE013htG^8)XE# z;{S3u!~{-&Lr zs*9P$Atq!Td2qsK4wih~?xBbo-_-Z*WV~c|^hK&4tl?TR5;bpX`c|b){*l8_aJEIOX2Nav$E6Uo@t4^PYd;W z@cp(`YTLAp!E?K#Jrd%}(Oc9Hd z(W9m6!}<0w7%taEaam{18ojX6ey+OQ&&(b%RZ3j+Q6xtB2x_)L5m9jNt3xDA*;B@P zw$28&{`6!Bm8JC@xUtrthe7e&QXg^P)Qf89?v=KZ*f`Di*-K5go$n*Kj7J#~MCsXfKw`MaxI>@e=$4^_$ZN_^;Q^G_*Lg~?<6VA5Uf^D#KDs#ShI_ZMWbjI*s{T4N|p9;>H zpxp3_Sp=h?iLdU`+NHz$i~6SC*OP)K3wNTvEI|rSdtN#aPF2lnK$aB0J&=Xc4xM~6 zDJmT*saVNH+P-_-Eu+HUudK=L7KQzZ#$CPB7B&;-3BMJo`sNCu`hE#D#8^({%-6L~ zo&9bRQIKPuec_RN-lLhJvYC9gsVqPu6Y;PK*~w-*NXlk10HdI_uLP9 zqV?ZS$l%*CiFXpXbG1?BL1_R0KmbWZK~xnHjhX909hsV+^2Bp;AU#@b_oKTssHMR4 zjJy}Z4_AT3*$qzu<4D@kOY~1EPbeoT%AU$@veNbn0wfqr{+3&W#7@n=zM3x>oq9V` zK9_^e+dqV*yK}~pIPZ*l^m76Wb!NCLa2s_n|4p!Mp65XmTxJ;?jkTnacKEX>=~pN6 z^}**)d&5z^;xIDtj<@^f^1sW0lJ@~zP!NhvRGndCkX~txPQ_CuOUuyYop{q53721t zzCR6S`@Q?y;XR02CL(<}np8J6HT7>Ud*l#L^yQIL3N`Wbsxk`1EJ?~yiXiQ`^`VhU zAIOk`(EV884(^eOdYPvh(eFA=YdQd5K%l?!&|=%q^R+lB>=tdN(GKZ$<}#^F{IDxUbln z>%wyci%(&Z(eK5XH&nECP1>;$XEVDSHb30H9yW~yky*(ACria(5qFc`DV}BzTRthz zhxP(_o0D+lBPPiE%T@_Y;+@EK%mBiEV@ltgMyz7+8R8ctlPT(FDchD4Q^3aRJRrCI z1%>Dr_?So|Dh7SE)_-VOailJb$j2T>U(Eei@el_Kc zry2*cB2oSN6}GLxwwY75e(~#v`|8C`>(`ktZ!+@za@;TTAW(Efa}~ z31Nn4?ZWo%X?X%8g|H73fp-8^#vmW7J0^B*Q>{c((!Tq5f%C1*A#XT3#m9dGI`Rf- z@~X7gcIwCq%bPE<0|rh7&nzwSZV*GYw`q<})LX)zam-<{f8;48az^05ieV@2W^=nO z`KvkUL_aWpq(-0Fo=hK~$?S>o8sQE|{yr3Q)n3J|(n}n@zDAGIbtFuc(;0hl^Xj(x zzUYB;x{Na;zKg8EKqZTb?sIJPs=$yG{>M+BI=sc|zzYv|*DlUVUOBDtdQO{0FfTqq z#n@fEdWb^$FFLa{dq|W6c(#BbN=+^D z{F8z&q-3bQkH=K)hO1ixiF~9)ff0SNmd|>;C)h|&@(rQvu4xTHsd9)EE&|eZU zhE;eTOTr?6U)xnlk|pi6%BWly&+`()QLcC4@w?$(Uv2%U7K>#i6(17i%^M40Lc^~z z5UTtdZ^qGiVcyeFQaa<}`cKTj68_Jcuxqb5DH0`eJ-eM2Z_c4oJ=}d(zBUdjwcPbD z3GyvD?Co~Fp1Kj7hbP^*_|p6Q85i2~h%K$)G;s4Ti(Pyc`ohXAciylA_gcH=I)UY71a z4DmdV)IcBM$x?1Egde|0+^`(KsyRi!b8l*KCFUR+oc6Ww4w*0c6X3K;pmkK3JyNQnpZ1Yoj!X@rJ?vf?0Sy?+9sY}QSnmHfc z+^fS2n<9+M0-Am2sVr6nf3yAJjr%~od>?ElzplEBOA5=CBFjrHer|(TM>##3{j)Q@ z-5~`8)Qv$A$Q^N%GidjQz&D9=J!B#9@|{i}-89Y5!2w8=n)cmk^(mj+jDJZ9EvT}q zbavPl)APdng`7x2Ds1o7`OunnqmWJ+8IBh(o>u*aH>t+`A6SX#juz)1oHzZ$KQX0m z($cez2dhHD%5dTQ%76=hV3~tMneqjryYA%E88rM7?-CJl$XxXQM9??@P1Xk2V?p)c z?$;br&&B*$jt6Aetp0QRdm5^7d8fjgMuiR*T4q4i{}x`Sxa&2TX%{UZ2fF+Go7nr* z=TTa%?$4SyDT%YnT*vIomxGz!<#~jKQaW%QWxwqWzd7vit%BjzFa>wG}@?2hXqNXZ23pY*Qcz(T}aKJU^qtm(@()t677q`P>!H582wgIWO zV_r=8>S&ygp28{~AaUPGjhNU{kH-@dyR2utEUI4^xN9ueOg_q-WYoR|x{)vhAyE;k z!hXEQ3CHhO3#j0ismsT5*0-*I=3v%)?CZy_z#v=Bv{~lIY5814pg>cwUG$>;qYKTvqf_lYGkG-clZyy$s9b;W$fyY_|#dn^wH~C(+qA%wFq! z@!~yl%qHWBRsW;36>Yah@vdL0M6pxaA+mx z=1K4Dah-0V$brk?Ez9LY-1t(m_#>~bA-rExvNPY>9cYqqY#V-A3c<=ep<+Z0k*aS_ zck{nm9s_HX0juT0>6rEd&L*Pul_yO5X?|j5ma@V_v&RoxCUA#WrGI(YR`Rdsqz z4WcGF?PaDheXO&Kh=aXE0rw-lYGcGeOXCe@advqcUWWTUT&(9)Y;8P7v~K;s5v0AI z5m!AP!l+iw99<1RoO=1^VD5Ggj**(diw`iD%Y{_~S9C4XcY-?O?{?NX8QbLm7vUlr{wu+JXfAEU^)Kk8h_$)NIugA=97mQ1^i zQnVm<+cDgsAvqD99KYFvRJ+r&aF!3wBkGGO^Iz5f_KN3Y`BCciK0ukwb!&u@n?Yzr z=XD@G^v`H>xVlxN(^dLEb5i&gUy4+R+6}QJOg8%1^8Nj~Fzn77G7}Gjy{}gt9=K-J zZjWQ4^!bzJo=OkQPIXKu9kd2AqtdO24muef!a`#LEAD*UVCEsAk2Kb2^8fgB{8QSI zV7|};`Q7L2Am&^TS^=_^sO#yz2L?0_zA-)?pe`4}CYddM-a$7_)rzi_Z)PER@=_UL zJz-`h2(iYc{@2nk0Gi2FHSH_*c$da?Pv9%&Sd(VI*f$0FQ7tzDo9U}K! zfYWX?X23GlUI@(%#Y;IwNLn06^XWq1!X=E(072F|*(YwISCuyX%Z`DOMr=cPWv`B! zVFvn!jls2$jW?k?^VJoTSti_)7A%`=I(Ggq$_-!^dJU+_dPL?|dLi5%h!m$V7j^S+ zJPqg)y3b-Yt?*dEcYp=a;dP>~HO~$La?5}P%0WYwgH3M~Bd5{;Muk^OJ;^f#`^7iRq zG>4ZL5$n}~ZylfEUo#eN+sC~svTbb_ZQeI2nvm);PODetNx9^pTcg3N=>w?Y{-_(1 z9V;=hMhh$;*TrW}1ztbL^o*xP#%*yk_WkrI|6Tqkf{FIccT5@7e?jIzsVT*w!hp{s zC1tSu$o#2u{8<_k)4tszJNqSSd0gv)`TFHsq;QFnkJZ8L-yeT=)Y zSaTEZIo{pMP0bf&O7Xhx$^Sn`@6}{inx@xn@7?hyeI{I?P_Db$kmEQPlp-Wjq0nE@ z|5H#Aicp9lQW81jICisps;hu100kt{A8)qzTPt}pb1~fmAoHBPzwdpYXRSV4OG@JP z?>kj`ZobsjVURxT3?)Lm$u#L@EB2~2aRquo)VT>mB_z$Y%`pFNg}U3&=zJ*FJjpK>5Lz=?^K&*c@HTvyE7j#8PIFinNa@Uor4N;;lalKtUMS0#9 z6&)c`XrjAZ9zH6A{6~7d;K-e5WHsH7>R`V`F%GVstAe|~TXGUTEw)Sh;F)NjT>7g|C373~K4@>~3I>XMnXuf4fXlgK-K zSQOahCHsCmDD;j|@-Y$6sItm+ZEw%Mt>$b8QEE6ZPudhjm(CK4>TJ|eWGx@Zl-!{^=TAX;<>Kdyxw1-5nOiEiz>cWK*W0(buoGP{+Okg4>G-)Z4_2RIM-hj!nXYdd zwLsRGvl(Y`7%DhmxaXVbB-0$SkGxch5VQdC@kFwH83lirFux&JfAYVsk;(`&M=83-; z*KEJ|YZ0X3lr}IoP;dAy)FoiY72XsCh`eYH4-YrZ>pMotz*%zK4b`)H|;P8adZFf*_oyU{ zj@8&=vWD5I$d~uz*u1-IzYw~=d8lLFOPrz@X0RB!vR$M*Yvq_rYwyTTueY(N_tOVa zbL8V!Up;j=t`pw1+{)689)*Akjl7Z#I#myaAqhr!A971D_OtG0mAds7`qG3>phz{ACv!$R@- z7`_>7Zi&m+hxbx;7olB~!yXpIh9o4S%2M+w)DuvH*>7ENWu^YOmEgE_>OcRSxFsgE z(^s3<@yPgH*T<{qexNwi<;#%tTqluK$`Fg$#I6jKh9ulf&~L!(CFdD~)C6&cX;K~) zCze{$x)3@}HtvT0jXG3uYVpum>i8T+=T?_XGT_8j z1oJ@P?Nl7wy$9p0d%ZW^nLOOHR4taj$r4@N$oBv7YXP8dbb`|g*Je?#;tVQnedFexNfbfVcd{&XjKPsApsCTO{|DJO7-nx zqaz;gHdtUWzc@Asbuy>oy!hgKr`erTC@@M_|3m0cd$?swkO?eUAH{b!oPO>5X1xO3 zPHJ@0xAxo+O&pzkilXQGCVV$S)yY-U2K43eWdgq2^VeaaU{-@_zv2Mb zhfdHwBzNz&kd=MPnrg7^2HVQx_#aIBNw}kU&o)N>pEqwdLV57j^pAf;oX5);g$X6E z^RsJ$PYL+?&#g`IVuAvw0&*+6LVxOzYxVSxsT4bJ65^3!7efCSWt#??)fXg_a;85% zi!p%iTdj(SmY3P2-+Hrax!tH;1#>@6OuuzTdJ%3qdx?gQrmk_SQ^ty`rNU0VSJ7}SD@w8Whc3BUt)o27Sab5EE0tmJ~hOG*RT9$KjqX-Kq zQB5xPk**z#ocpjYDZ@VdZ2P14kXVCnfBrFZzYJeLt|!8NJ!mShXC9$(5{f6&#czxm^k%a@5UNNGa$(dB!AT2gZum%3K>9;Vqlk z{aF0{Hb1i0SH{3=gT>_K-Y23}0ZeM|qhyGN(Dm|S* zc{ASiC;Rez`h1q^61+?4N9N`ygFQoA7`W`^rrrmJcML;>!ME$RQ}ZC6?+SUhH>S@H z31(F(;)b$%DEXH5tBh5pYy_t4-DgEM{ne8{{T&Py!U}-OMb$d|SLKWt6MU7gCV~0w z=OJYxu55ANwECB9W1d+bX3a`nKzf5eX7gM>NY}up%rt({U8AWspX~*4=-*Nf;7^WQ z*HuOz`M?vhsMK<*B6=c3C76}FqILnlSYQv-YigXmCEGJCPG=}|`M-B-27$7FZRNYZ zH8#hl&oh+ndrHMisBis#1VM}J(GuY@*$%@^F>Jf=JsgdN|D)y3Z5f?GA?z#4_1&X3 z%6+l_xjw|g_Gceo?+0HN?;`csPPW0Co`GDbi*!g-i3&o}i3$@UF}6nwqobn%!I)Q~ z?D8JSNUlJPcXP&I>YBY&%EyvZ?9Q7Y?chY9PH#iW@Wgrj90&NJWWCvM2loC!^3&Kk z`b~A!>!%&**Xs5@CkH$fS*k1DXt>~fpz2W~NSZrVoxQo)g+jk9x6iP zb^`U2`O50aLZTKg>Aybu`DY=4ChWw={#$-?qr?cEs3Ow`E2ZWoU8c!1WwAA2RzWF^D}|w;D~yl&T62a%L(-wmc(a}4!J4C+9Z>XL+Qynmg4(w({Idm{K`qsF zjhwrK;Bzdj_luyPZd5TZsF?OsgqzW%%R+z8WwNej&8Qxon)&}1I$<@UxwpMi`#1f+ z`tbVBe;E{SsuQgYRg#q`#QZGQdh(haA9V=X>j|^VjuDS8MMw(B&P1YMJh#h_1U)sM zMo28tA(+G*OcYAz`fLbXdT0I}`(+g%s%suHUf;F+ zn|j}R|MfcjD8Y5Dq-_GOcItlP>RKX`R~i^AD4n-Xs`F=EQz8} z@zF#^X6cwfpR}?SvmZxX6*G_LTLE4(^Zf-hDk0zW#9;&jANY``vtgEZ!qze36%`50 z8l>=yVwvX$<@k`N2oCot)25qJ>rrksM@$dG(6l{iRuJ)nT;*DQhf88o6=ZT2UBmU<#;+KC^%oT7CP)v-bN-x#ea8t!7-* zcH7Jf)pIejr%E7Qw8O^V<$ui6z%O`ScaeYxp!zBqeJ7ta&A}Jr|KowrxIqpSj%98? z|LWnp9W6vjKbdZy_~{S-0aXqqsLOh9XXbC8+J_FT38OWua?{BVAebVcKHtl_82>y8_tmCjNqL2I#B*U6=;B4*j1q_G; zv&3b}R=?{Y>(?Las@#!vOU;=rji<^kqRR^er|TBxVovP6gZLn8W~8ox2y$I;nf}Ao zocZ~lwgZtAd29Vr>n-1L=~kNIpyjl$1)Bdix1#XXY56)x-FAf zg7O_Ts@(2^%#$q!Ct4#d_oqye$VpS<;D6DAU0R69!H6OhP`b}J!JZCTC3WE!%nzoM zg^2rkQ&TZTzW;*LNK(i)j8fq-c;dmqU%B-g)9}#q~lCgD}@y%MQqwZRO zWGxM<6V3`5G*NAC4p#}+6RACn)44H-`S7zDxfs4#Ndz5Ygx(v^p0lXb<8{x#Bt$yM ze6^e1XU+Y-kk9p)i6>765x)cz4oNnhqo>dQ^pS0_^xrmD9+323?tizC3ZA~49s`W= z|8NKNQ0KBIQ3d|u{7;vQ&Mz7p36aX@P~}sp-rZEDJbXh5bkYl(XY~xQrgb2OE>>#T3I1$b1bA?Qet$%`zwx{JigyWV!IqrY_ zV~IRl9T*~a6jAn0|L{=ir(Y|tjgwyd-X6fQg4pdeCUJRjVr2T&YbmRn zj4yAS45QSywc9NW&LNU}JmeZ7Ta(dW9=!4=T0d&!o}P?$$#@`v^A$nF74y zlDIBu|C7*QgupNaTr^-|BN`?-BtH~RYE${~x^&n~9b46%BV&d`AG~QsSzy5r@~#qf2-m*Rwa==9Um9_VzadlO9sds1*3xL zg_Va9YG_aXt@He|pZ;#Y&iJlyZErMT`di_@d^9Obc;=IyKN_sw{6L59P{`{6K;6F> z{$aM0HH}a{g4ahz6GduqTS}QdeEq)iTlPxmM)FTZ=*-5A~pWDV)`M2r6I4J%2bUaVVHQ*!iJefDxG#dVT3vY|2n#0AbC zX6?40d?JdCb1*!7D-Q_Iv{3|`b;X&*IpPSX^_(c`N&XU^6S4iCm9ItR*&_}U59?mt zbj|QLPA((;OMT=?f?i$3`IX4=s`JjRA3iE)T=bh&l<*~cJaQdbJ24^^Y>J>}q8=&_ z%-bigzW)wt7k3H}SDI!IFB4vaj^ehi9h$~4*kblfiso#58;rtRe!l8Ork3imRAK7) z&)UCx-2Sui1yjWY=6UxMxx-HP%BksjMq=Ln+HuhuEsC!1Jd^(xW{l=C!>2807S}Dn zUpjiN<@!aTQ(`vMWETUisjfTqX+c)_6(Z*-B$>h=!(yx^ zWipfao5-8ZxO&(G&hM)Un!jD-H+jThL1SAiOVAtX$lE2&63k?-{GcS2c$w}g6LbrPkQP>Jbv=s8*=Y`*rXWEvc9*| zwBCYnk7xqGZ=(G-ox)3UrQRR^CEo_@iYrqVw{pm3rc&H*<-ZMjf8sVTqDH5r`{ev; zIjlR`+Ks20v~#=P$7tq?91_vWgL)XE}wC|Bq%?B z_VRs0Vyln@iTcn=6gEb$!1rn&2nPXji(;{_d$a)~&$yx71dSh#bOw1)!QhSk{I*)vuP`H&s} z`8lyJ_XphFc9KZ$ovbTFFx+u%rk{L#$2-k{!`AKPUVog#){Bh^+XY=-;(X`^B&h2m z($>>^acqwC=P~S`CGE}-{p^QHkrKerZK?^y* zkQR*PEI%1ud|2=e|I81B1NC+{UyrR$?UTuiWZ7qe*mM9$Hxlj3TC_aBwHF zgu53$bLHy2qTjrqD8^BYx0G_cb@=PmBOL?I%}f97xc<_f-@n{_J?Lm<_=m|drDf!_ z&Td6btCZnjW`d1#-T}9E`WOG6WQI`9ZDyQ_kw5vwrl93YLSjUdmX+%nn-eV%c=bMO z_iK#GVul)K&9IH3d;@lrG$$z>Zam%bq4Hr!jC0dwc1ijE#6Xm)80+dxXeIXir6ujb z#ABlfY@;_vYpP3I;*aJycsDy%8qo}q!o`f#^pGhn<9&F5(^t~f#vh&A`{7qNMWxYc zc01+mjOe{$_`b1ypg-urUgb*vX!)#>*UqJ33C3xyuJ=%RyJZLstidCi^A033V+k`* z2WoQ#fo4@Qx^Iw+l-$h|XS9`snIb8BThW0_@@ZnVy1!{pKaEb|+qzp}3`?nQjPVb{ z8_veUq*=T>=5E`6u=;^iua0Y*J{w4`tXaXAJ}_$K6^?OmpP@lcjLaC}c=o@a<-em# zi5S$YwO}3^gGFhVdsY7=bp|X_7nq>Xp)|6x9>1Mp*BggNCq=Xr&+B|G^yL5L-(5lE z1jaaWUBB0Vw%a#dG~bqoryqIpegPbN=^o)rDlDZ8Z%D|uMWM$zC+d@Jrb8@eYfLUo zvksK&Xv>HS?wqvfpd^jgqngQU!kJ59`M3D2KX{(N-InD zicA?RGBU~OR$NMn-C{f%>dyq6>ceIyE=1d>9ftw@5o=!etD>@|eKotT&5@voaqbI~ zh0!CgUdtk74&7hKZHcdOQI>{2&UTyNt7C1K9XOBudS_}vSN`x|Q>8fo-?W~oNI4y7 zdBG^oRNDW(L>?k(#x{thYrbJH+*705eB-X%eEw)(7?sXCFki54!AG}gSB*}43?Vh8 z5Fim+tJ-qmZU9M3kx)_|O4r3#&S`HB^SzF7o73W`^LdT#0$QiJ{Z#woL6J7`jsZk# z*h&>qhb~uOMBE*R-%LmE97bd|+Tbt#Qk(nhQ2eFxfxgCp9G%I?}V>TX6w}X zmdngTdG=^wQaB9#xUI-(p$rx~{p7#TmmK1R9%5Uwp~fAkTj0E%ePIhf4qF~+=96rO zuXbM7o+EJ`S2ZE7qhw^IT3%&^PO(G++eX&Qq@ot6Q9aO@`!rOLD4!|&#kz8(3 zh2wl2r3%;1yoBX9g8R&OASD{^Zr4Wt_~QmpMOhp%$!54)a6qe=(WjU$@lf(JIsj@1 zrxbajTaC{f{tXE$E;Mh*zSH}q=kB;_jO}GJ!~jz_{J#p*#ybFr>%h_tBhnk9?whcb z8L4EuDrwcjFDD#)>l6C-#SF0en+x z+df|&NsP{aWP(Kn)5M>bjJgNfi)6Rx$u+%iFL%aU!vX@;%W?X6#s>vvG((pw0IYAT zcD|#E*i0Sx1fD|Bp=ZMXHnR+-d>65KXW1?#>d2GllN=drrIIyq_!cR-y=U+?PLOp^ zstsSjT0U%9do*Ny*h<%A0$sUu31NwC+&ldqP8@I1q(>yNkz`c2OqxQ6-%DkCM4Ohl z+`H2IRr6p(V(!B#I5wMyc0RaJccu~$^ySQODAgMi`teq;oiE$X1NJiEtNMF9d0YN& zB6Xy%%D;g6Ot76lRvb-0IO+jM z!c8gEUEPMJX!i&*n$p{uIw{u}IXt8K1nZgVh24+c>16$gj7->EPqs?9Qnley-9_4T zsq0G<0(;pR=E8J)EZmkiE-J!6N*X<-rk>~r#iwewZ-mA?SS`U2v!UN<3i6+p7I;K3;D;5nKwit==Z%y3tdP&elIh{*aY<(9VGK zoxKp&f4C8Gw2Er1kPcr8l=a*D$~=DZm%aZOQy8s1bY9_#mvuYTZlbsf1Z$Ph`UiE= zVqV2ZwMgRj1k8=JV@aZ~7>6`{rj%c?4D~>cE8JJk7Kpkk9WDpvF=A z+_v($ch&&kcp_%IEPm_)?K2K>YIlCNeGpGLLAXiq6VWRFNm2cbSoVNav~gWZ982k^!OV3UNN{J~H8++~D6Ga_TBwx4u-Wz`9&>#WS1r34!!HyB(P} z3EZliE(FQUHAEe{wN~InaJ{Z$kdy}HpSBI{$4`>5l2qft;O(j0_$f?4AESG9Zr$@d z0Pq|eUlHf<9P^=WO(a0l;FR2F-a|Ahur%D=D+3~w7(rO*p|I{R`Y zoaW%kcDDxA&lFUhCAQX`oT+brWFIn-ExlX^^P3N~nr$hVXa`?D{hNP_(4@BAt!=vI zM67%M^x?L&&ic*u%=d|oNG!}&T{t~KDrrM-iSr$a7B-G(?hR0D7BIXdQ(0^V1|JY} zs|}=04b6F2M(Zw6kB2v_?n>_^8Qd4fQe<^08>U=kmTjN-YYv50;K0@7_i1RKuS@mt z+2zWN4(Lf}Te(#;pablwXTwbQIwoqh^|h6J=nrrP3E6A+b0^yim7dd&l@vQ@X!zh5 zu**tB#wqgCl4P%z4C%cx>?LdY(9f=ys#TX@ErwY#SgkztF?5cE1H;T$G;hgw-PWT) z8J-_LR8VeGzQ0&h1%zs@MEesU80Vn_66gF{gi{^PKt~KLev~&j&T-Vvd$PPOH^2-s z-PgtU39w`j3VjYVI1LperwqB-7<|MK&<|CU=Bo&2jdaj-b0!n2XkiKSkl%Xxjs>u zXc6~N--r~q(z?u2?=91T&(A=bgPJ;USF_qYbRUe1&$sUxj1T2|2yz{6O7bLiDQphF zzG05fo8#8Jp1TtPG;K3Fi^_X!bCMFoc+9aa(9#P!1~If>Kmba0nP)h_~aOh_aM#H+C|+?B^C>GxX$u($2-osHrZ^a*D7 z-AVs1zPS4L1#~s`o0MiCAX>KR7{bHap9uHpLmBe(Agn~17ujyzKYvsNPDCE8)J=Nm zvyXop?&-oca6qU9V`NW~)azx^WLeZ>)01!cLWL_vjZMj87BWc3^1s&bq|P*@+y;GG z23a{^id+@X7T1}Y94zW5XYvnFgo3H^u^eG{eheEWVQb|l{RN2AdDCOIvyi5u#re%0 zvRw(3J<~p|Usim2A6T){8?s7P$uYDN$3KFGI-WS(dCN`H3kVZ5nlBb>XUqt!#s%7; zHo|+w(o)$=YL!``U>^GtZNkAG94QHgT`Pk!rZ@2Dt~Al|^bs3f<{onOL^`*AObc$Q z4-7pkX|&+TVACTj_*&T7q4rfGS_Q-^yHCr@yD(^J%RG#XmAA(e|el(9>T}~=M;-SQ^6(0!MA)f-YR2(2Xiwx zN8eUbitd)V_2oNAl~Ux-?O6a*Pe2x8qn%hReRV> zL?Jj5wmbG`)KGaeU|3@}qi=tdk7pl5bHMVwabZp7=YI}O{ceS=HOB=77=+K2*T*?B zaCfa^SJ9f{G0Hm&HkR9N)5IJQ9%IOfk`I-OM5{uqm*%7YGaudDhmT9N>gluUr+aH7 z!t1IIcysHBhDm2%RA5z$K;>4yLR>pFP5w5v+tgwVL zA6O}Rnbe(ny{&Hw?kyGQA~qScWK5?3o-)>4(=#6rH9vkJ3kO;%TeTzMLVrw833~2DA({D zH2^+px0Wxn?V-A*7wkf=mRuwn?a8tE{$n~uUaAPPXjbP9u1E}Z`%{0(4LaTck&n~n zvxD}%W^=<(D}805=t*O?NlLU2*_9{fW`_D0hXzp7`Kv5zFFzisBA;ybh9UbUws2gp ziH5iOVx$M~R~@`Ihc?(Ka1r5c?}>MGM_W=bw56t!=cWP`qSNELH8{VY)28z10=E4< zSh2zIQ@q?rPXRpF7E7iSbQ@$PMWrkf1wr+(=OLX+&0i)zq#eJ=V;h)xr+-&DoC_o%|d#$Mg%#0*9nf%TGRXi}kW-ug{h zy*;}a%HQ<6@pE@|eV^1x#Oo>dFHZlICgORu77qaUGO(?g44Hj@gyziC56+d#D+D76 z3@U_#rgLtK!ylmR`N`N`tSFXAbdkck$`ub#Tb1hFo&B3Q7uGuL-MY}fdiKK=$?OeX zAUbw6L=+(S$V|#a8{wH)kEt6|sW-K+qpjv_^G+;!6ZS?O>83y}3UNfXrJ2YTFJVF5 z?_)XWKRp!xaYLSGXge}>dff@vH3SPe(2v3!PK#{4@o`kcN%g*+O1nU0S);)`E2wUp z^h>f4@JK`Fz$1a52GbN${pwaZ$?48;Hk-b>hWb~v#Ps8Qk_<>f$gIOe4?ZZCv0_8w zqgtoque4>pAi*YXNiq|6B?Sw9a({?1F!ToTOiR9SjLG?+`op^=ei*e|;l3D-o}V=P|Ma6E4=LMaXatZb z_jwSZ^%jj$?GnD>@Zr8%{}ERpBe}tszk2_sKCbUutst;t67pehO%oXbdmvBaReHFg z7Kf6ce>P9sVqO)+)4$L+5u5bTaEz#(kP+x?{OF=j?N8*+XdJy%Ut7Vnx32rTuu@IK ziMcE&PH!Z)H@QEH9K1|krj0&*s?U#0)M`6!;sr*wEYuwUO=@os%oWxHC-;lKRqPz5 ze)AGcv>hhhjXSxM0<=K2tBZGIfCd7i&}{^#$YwGf^E1_88fO>(gLt_j*P?lZf*KPt z1C(IP1cbp-M_<5<8%fOr+VI9_3CKFlJ#T%TU$|7Ho8p8EE{BC|X`LP>@E#R(+JLnj1u$JzfK?Sr7OzXHhCE zXpUomn8MRmTHi7XOf16w%-249;>C+M_p%Ofz9PvB&>W=ZnzJNrw!A)+RN(A&wNk%) z2qjZ#g3~{{zO1ApzA%xv=hXtlLIZC_l+4(*gI2o1+_C1zq1lAr^cK2^q)oqsVnz7D zsFG2T-}t&D%=TsOv9dg$pgkVg2BQEtcoaF2{Bl^dF)gYD( z&wG}0-6{2_On^9k)o=xfXrc*nMh=;?8bc5ADm=OcraC{d$^Mr#UM0nxK7nE)uH~aD zK#WPj@}*N^C3fkCJ_dDav;RaLSuBuQp&2>VjB`HKTUbL=BlrQWWH{fIqo6 zd?N8{fXG78V@N)JOL1JK+Y@$LIv~UnhOAc!&GPM6BUNtph3Cub!GLiSRTepT;*r+e zClIGfCP0RO;5uiKP?12TClB_Z1gE3TD4*BGW`vf$=BPu4NMLEr9*Z4(M)OG$2jgV* z=v7X59l3r~JAKbU?Oqg*nZKR)3^Q9robL>f2Sml6IMQGTbt8(vny?l(#}@DmW*MEKvNr+1QD8R4k}?Q-mQFT?M+fM=rOZSt)i}zcI`QRd;2hpP%6+>;BL6-L$k8kLO{VQ>Hb77F9RpdtDyAMb{uQK!|N*e zvbzo8fk0_AvVp-e%mZuLSyjL`EY8oqIJ^B}6BpInMb;)D``h*m!Zq2_-qW}qxQyO7 zAn)8y?2T+&XA74>t+G*wr!|+2hD}lH$oZ%JVX*b}hen(mO6H28IPmmIPr)=W*~wlA zXQMM=tS}R0%+9!GDzt5aJ&yD3#9t$H&epleQbIoTTA zU)zPm?XlM9%dS7KmE?lJ>ojnnBnZ*#o{U=v|BMl9dLr1?LjUz@ z%Sud&4}gtQum5hdfmdf7>?71B#l8$3F4GeKamGK2Kz&qe#~*`g!}GUh?8j?H>MQPP zKO63{qTetIJ}mJq3OjouNiP=O-iMST@G-~CSqL8)j*rv!%)}i402vKQL_t*b!=2o? zD5{xbI_vvXQbxoB`2z$au&ZOoO7xyW_nfGZSlI_JUi_aMHXSAbb!i=3G^gnnZff$g z{nuLc3+JDr&c(E-+0?W7xO9og-eAZQScHq@B|XkptQ&g=4~MZD`%u4PAqQ*p`9eINWX0hgywG(V z1*t}`(@E_e6A4it

    Zgq{AzZT(!zO$gSsM%$bUf>ZBRqINaH`aPYWsBEXhr>HuP- z&g~B3Jrj6wS@j)VW0;mfh*vdEAnO6j7d7Y*)k|Gcp32D`H-@N&@Z<5S@Rf;#ol!5IL^`ZJ5w4afYl4rg0*RWq&SFenVw`*0`PayGwjH)O_nmLtCDSGYKO)-nb< z<9gF)gM%1>LqR^ei?Qo4ui@`{d!MQ`HxkCW9Q%-k??E^;+HLaJFRM3yqF&uT>58j@ zsW)`>OoW6884M!B;lb|1&_AhfHp+m0T5Bi!xqxEzv|U6t$2Q$&;L`uFO=ZzAsemaY zvJ2_6^X@-}qI^90378k+6RYzDBjem7223x+ZnMftAicygddzykimWhastoG1TURca zRft2G51TY{PSFA-Jnnj;GN1A?54j??o%`VBmp9vE0r$2Oa9>eVtsn)JvTcR45_rN+ z;ZyRwKX(Q|;pV(m7^89p6wLH#txvxB?ZK;)p62*61*bW7Yep-Kwk2cKWLn0lLR+{H znOk4bL7#PXHWgQQ{4d^~(6xKWAUR2ciDrD*f}|MRscP?ccwH#*thM!a8*`ELXqh)Ceo&ZD=4^|$Z(=i;L&_R-Z;kjY4i?2$_FpHQX(AH8Jhc2p&D?bE@ zAdR{(b=xeWiKTB2Gd}k*x&35Jo+L^g+}shgy1Pv|G%1DU)Sgj5yYgKvgeD zvtWDxGSSv+@z26@NY^|9SgnBX*pe0BZ?Y3L| zG@wZtX#*2ffQs zE5uh{#J~IIUxt^^;auc!WD3*VQsr0Mm2P!5e`tTZfN;7Tzj2qZl#%s%cXV6W$FK(1 zGjGuh-9%g4QgOf`zCJIyt-{19w1O2^a!>e7nr*a(sqS`YpEH=HKrJPWVtX=~>Zx;= zCXLOXX=-hL=-o})D((R;)i@|I7S$Be61DSYR)Kcx@l6Mr`KUFn7f)-(;z+OE7s?m? zf0JuOF?686B0pp1RMl2EdT z?opd@cU$a4Qj$Tj+zJ+O_9au6UFF7RwaDAPZtr^+n#SSms_vqE@A5yN{Pmxb6&S4|{t!-MB{MnSo6l&V{AmzP42S-w;4#oa_IPHx-C zq=y>o-Y?=e?r+22H+MXkCI{Z9cHgdTgDb!leAj+G;uOV42g(i zHJIFJMn6{R4C{}~8ooRP_c4n+uqNnw$}Cm|_n{)z!KO;0Id$2}2eT?JeanCeN6=Q) zdddoYfV?rg@np+H1tfAOhg;vjXTCE2fbCdSOZo+cl}9@kQ1!18dioJ6VGig@#+kVO z;x5ewkJ^Ab`WMy5@x$VYDBoNPw!`Ud^Vw;!M-8|h3?F{jRL(_q6|J5;^)}0capzE% z3z;;NW#-0@{Wuom!x6NFfv(xS?~*zUpIT?T+2}W=w>2kG1wy?!-|c3ywUP#Y|&%(%d~}V ze6G!|01oBG&K>9Xq}Nn~@GcRgZ40-Ctur4khy-@VULoViUB_7c*P_?^S<*eX$^J;24|$`t8gH!KaND*Wwu|V9@<3a%71P5%jwTn7U6_@&Vni-RE{MJp7<@trW6+L7k zp1j~R9y^%stzE1g)wPrfGII$a_b#>17R0t5muA{bqr$HY4bI2e52|}Izw5>hIf=e9 zU`{(rQ*QJS4~Oab|1C&cXGFLMfFO)`26a|9K!B^f=&er$nIC$>aHe;==o9GC1}cV{ zdDBv_=0S8Ef^F@Ev2T33S9)gJ$oQ_HVr5 zj1~bc2yRqSLd(tXwI~z7H-K>$eFa&W_W42HU-PtSC7tmD6&ndWl5DKqUwZHD+0SkJ z{FlujJc(~stqi1V)U9IU2>K%bm6YxG|D0dpl@HUqa=4G-fK>&GAPg46zr* zJ>cP>iou8$ zDY0w9LJVu6?DNrIDew*+jd6G}Z6k@AZMN$H%x)9%{iJ zh(Fp>-iYiK%7*N}^t& zdtc2~mrJx-o67ec%x-1Xo>ZB)vcSEkqs*QBq}VqrPtK;F5nzMP*&j=XlA|&$&aea2 zz%bc^oyzamtI|2D)@o!;OCYh;HeX0o0if8FzsE+{STo)?LU+cbGf_p!6Fd87^n9wv zpLV!gF}UbEG#=+E!;K~b2ASie7YH%a%&d_nx%zkA>EVbJM6XH0^|&qZcb4$xo}Sz! zc}UhsC6=xEVUY&>Lh^`vZR@-7(N1VA9t`da{@Cp8Zjh77>l+m1MPpKv-rn%^V0>_}w}nObpmnWhXZX-yNUIuX z5!K<)f~Z9|R!@E51inlqou#E3_L2+r zXWg5SVPX+t?srmUF9=Yi@8ZAMtP<~w`~Wc>zO!^QIQ;%}dPdfSz#!~pM2h(m_c}01 zlrl?eO&z9Sb=`JmGXtM#&pUavvWSX8MKH4!1p>~`6ib$!>$yvvaHcLwpZyayzFoAB zC)1l=Ujw+f9VWGh{9$UsLtpq-FFubLwenmTWsX=0&qJyw zk`dI7-EBYM4Vw+VwSQ{d)M6UFrq4O)=gpoRKSdCuo2E#Wy?NT552qQ9Yi%H0&n5C~geK>o_x})xAl&g8*aE=li+oAVj<|c&N zZ}vkZ92e^tdMn>AY*RwWHJYk#&bSI2(c_63w`$w2f0`e2-*43x+S~S6$<6e^X3fkH z7-WmUzdnfK(z)iK`k3%NSV>8~uMojn6paI97GAB^`PPLYRaS8Qgn-pDy}8JzH9#m? zsWcv5Yx6XY-I^PP7&WehIaks9Mp49EudZoPfmHlZE!{Dssz>wX__`ic8_n_BIX~!~ zH3qxvOU>e7)gH1A37a#~t z1-}#*P#L>XL)MS-uvJgbPG)$kllzKF)E95rXz=EsA>5k%(ba8@E&0c7@xqCrNiw}A zlp_ey2d15A6ZL}M{HHa*5&6c$oK1r}Iymx$)%U)>Zy$z-vCq_Dwc>UV&OVGktgDfy z&}nuT_6+*a2N>o{bf1oSdzEkaL>6W#z(1r4tcQNoAUnp2(BF z4CGh~xp*}iRPMS|j|%ON`sn?xsU}OwpXZec7&Hw`WkddXQ`M>;DNB**@S1diBs?_c zQlcdj6W32q9;HkmP#>*vjE?3fgihh-s&d**;Z9M5sI*+YPTKTe?>Bx~P3^mBU*=@x;ohLHTMO2aRtrbxN<(%5#9$Mtu1=u3&kJrnj z?_6{@Njhy~^)ca#QQZ%mdb4a+O^~y-&1bZbAs^apw&F@G|4*!`crkR0KUhb9slDE6 zWKahnee(x`ohQ0u8wzm8XUiybtkFo_*KIP=cYBU_v!pxGmm`=E4k1V5(L*ed-U0*4 zY}H`XK!tY-8TsJL!-v0ZSU0wN(jVl%sae%nV3_kxc%=ebsb|X^W%s5xR^wGIM-0_z zfvZ4!*Xp}UJ)dad_T{v6MNciUI`-iR3D)%B<{et{Bl~_&@w(^9HAeYNi_FJA+9nbU zUN6g*RE?}5rydk8nr0U2-qrei*b-sWb{n?dO)wCmj#E{*1V~PTx@FzmeCE=r$>NYx zSg9@S>TTiB->NvHwlSYNDV-VWywJCEEj$~)-V(F6#9>%+ypzP1^cOTZY~*ePrT&-OjGK89OQ_72i@BgZ>$r{&@B?kb;rF7xtL zV&N9~rVP^hpDQwV|KNV!$DV{7 z#@2wR@f~kZu)Xx%1hZ~kZLW~2!datfT8U2z>?6>etA*Ts60L0$oax$6zx}iO$eHBX zeWQD4va&8m=jA_LO5TySf4e>HR+9r7J(O}#gUQ*l8_VrL3(_#h>6TSIzJ;EJB}%HI zZUVBmLqr-?8yG~75~1-f8Z-S%oZq~U5IMDR# zP_odG0&1)CO{F?Vvl}5gqQDxEgq!sX$H4gHCNT~r(2GnRc#gR;+8M{j=IxCB3mND~ zQn}VMN>W>GhvAk44dE)d-&(9uTl&2f!yzU;`L3{7??1L(8;o1Ilp<$K zw^jj(*7#+V8nrNuXm1$I`eYwbfzibk{0&-x81gQjx>vWNDn_h&vH!Jq1j!f4$m3Uu z1&P&2$>TdZ`YZ1(TRbYwR{zEAHL!6aM(Jk*Bb}2dkn%-zIJzxbA9mgrNH7v^h6nTh zWPSZ$TelswQuKXXcLa5M_U%8$1A96IQ5L@1eV;zZ1*~8wW=Px&`8hOaojSdU>IYUJ zS(mI^iCI_I^|UGhJ0VEJz0zKDy*dAp8qte#@b>I!y$t%5aL4@MRDWxk>Pi0HJw;9e zm2tb3PWtIGJce|<|NkpG*X5+4D2(P#LP7!oMB1rVslzy<<3srL{_=;;bjDFDZE0yP zrz8ZLdt7-0h9qb2efGE3co2u$YsS0plXRD}5kY20Nwf2dRj!2IL97(UkX6ESOZI}5 zNmXrv%9qPoVy?gZC0J#IQhbhxsA$wuiNtD2Am=_G@%d$UXc(O$t4B#qUaIWs@MKID zndsypXIq1W=Xv%5f;hw^Or(oRgRGzIdxPYO_FSCi9PH5Zkl0iM8Oq4?k4u7tgM;8DR^^hk6|2dKRl*snF^p(f#asF-Omu{JOy^`IQG9jrsSHmZo+s% zx7V_MeYU`O+2l^~vDk!z<92I(3Sk5qwFw!6Db4)2n`8E%k%jDG4pT0NFtnw$AK&IG z8g(Vglq0|Z-MXlj_F6N3Bd*WGY--xf#b=V^+ywp$`{ps0l||LZD&p@7FwjUpbX&wz-l_dK+n_$wGL=8ACWZnD1c*Op4o(xG zWb<*-R+7;4E&Oeqo?Pj(9^Rr|E}DbO>SL>njPx# literal 0 HcmV?d00001 diff --git a/skimage/data/rough-wall.png b/skimage/data/rough-wall.png new file mode 100644 index 0000000000000000000000000000000000000000..8e2ced601e455dc69461f635789365eca7216c93 GIT binary patch literal 197789 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 Date: Sat, 4 Aug 2012 20:24:14 +0200 Subject: [PATCH 272/648] add plots to local binary pattern example --- doc/examples/plot_local_binary_pattern.py | 27 ++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py index 9ef262bd..b00536a7 100644 --- a/doc/examples/plot_local_binary_pattern.py +++ b/doc/examples/plot_local_binary_pattern.py @@ -12,7 +12,8 @@ each other using the Kullback-Leibler-Divergence. import os import glob import numpy as np -import pylab +import matplotlib +import matplotlib.pyplot as plt import scipy.ndimage as nd import skimage.feature as ft from skimage.io import imread @@ -57,6 +58,30 @@ refs = { 'wall': ft.local_binary_pattern(wall, P, R, METHOD) } +# classify rotated textures print match(refs, nd.rotate(brick, angle=30, reshape=False)) print match(refs, nd.rotate(brick, angle=70, reshape=False)) print match(refs, nd.rotate(grass, angle=145, reshape=False)) + +# plot histograms of LBP of textures +matplotlib.rcParams['font.size'] = 9 +plt.figure(figsize=(9, 6)) +plt.subplot(231) +plt.imshow(brick) +plt.axis('off') +plt.gray() +plt.subplot(234) +plt.hist(refs['brick'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +plt.subplot(232) +plt.imshow(grass) +plt.axis('off') +plt.gray() +plt.subplot(235) +plt.hist(refs['grass'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +plt.subplot(233) +plt.imshow(wall) +plt.axis('off') +plt.gray() +plt.subplot(236) +plt.hist(refs['wall'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +plt.show() From a6bdda82910eff535ed81c235915116b7adacf8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 5 Aug 2012 19:19:02 +0200 Subject: [PATCH 273/648] rewrite local binary pattern in Cython for performance reasons --- skimage/feature/__init__.py | 2 +- skimage/feature/_greycomatrix_cy.pyx | 67 -------- skimage/feature/_texture.pyx | 180 ++++++++++++++++++++ skimage/feature/setup.py | 4 +- skimage/feature/tests/test_texture.py | 69 ++++---- skimage/feature/{_texture.py => texture.py} | 85 +-------- 6 files changed, 225 insertions(+), 182 deletions(-) delete mode 100644 skimage/feature/_greycomatrix_cy.pyx create mode 100644 skimage/feature/_texture.pyx rename skimage/feature/{_texture.py => texture.py} (80%) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 003a7b27..4acb678e 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -1,7 +1,7 @@ from ._hog import hog from ._greycomatrix import greycomatrix, greycoprops from .hog import hog -from ._texture import greycomatrix, greycoprops, local_binary_pattern +from .texture import greycomatrix, greycoprops, local_binary_pattern from .peak import peak_local_max from ._harris import harris from .template import match_template diff --git a/skimage/feature/_greycomatrix_cy.pyx b/skimage/feature/_greycomatrix_cy.pyx deleted file mode 100644 index b9ff7f23..00000000 --- a/skimage/feature/_greycomatrix_cy.pyx +++ /dev/null @@ -1,67 +0,0 @@ -"""Cython implementation for computing a grey level co-occurance matrix -""" - -import numpy as np -cimport numpy as np -cimport cython - -cdef extern from "math.h": - double sin(double) - double cos(double) - -@cython.boundscheck(False) -def _glcm_loop(np.ndarray[dtype=np.uint8_t, ndim=2, - negative_indices=False, mode='c'] image, - np.ndarray[dtype=np.float64_t, ndim=1, - negative_indices=False, mode='c'] distances, - np.ndarray[dtype=np.float64_t, ndim=1, - negative_indices=False, mode='c'] angles, - int levels, - np.ndarray[dtype=np.uint32_t, ndim=4, - negative_indices=False, mode='c'] out - ): - """Perform co-occurnace matrix accumulation - - Parameters - ---------- - image : ndarray - Input image, which is converted to the uint8 data type. - distances : ndarray - List of pixel pair distance offsets. - angles : ndarray - List of pixel pair angles in radians. - levels : int - 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) - out : ndarray - On input a 4D array of zeros, and on output it contains - the results of the GLCM computation. - - """ - cdef: - np.int32_t a_inx, d_idx - np.int32_t r, c, rows, cols, row, col - np.int32_t i, j - - rows = image.shape[0] - cols = image.shape[1] - - for a_idx, angle in enumerate(angles): - for d_idx, distance in enumerate(distances): - for r in range(rows): - for c in range(cols): - i = image[r, c] - - # compute the location of the offset pixel - row = r + (sin(angle) * distance + 0.5) - col = c + (cos(angle) * distance + 0.5); - - # make sure the offset is within bounds - if row >= 0 and row < rows and \ - col >= 0 and col < cols: - j = image[row, col] - - if i >= 0 and i < levels and \ - j >= 0 and j < levels: - out[i, j, d_idx, a_idx] += 1 diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx new file mode 100644 index 00000000..d79fa6a6 --- /dev/null +++ b/skimage/feature/_texture.pyx @@ -0,0 +1,180 @@ +import numpy as np +cimport numpy as np +cimport cython +from libc.math cimport sin, cos, abs, ceil, floor + + +@cython.boundscheck(False) +def _glcm_loop(np.ndarray[dtype=np.uint8_t, ndim=2, + negative_indices=False, mode='c'] image, + np.ndarray[dtype=np.float64_t, ndim=1, + negative_indices=False, mode='c'] distances, + np.ndarray[dtype=np.float64_t, ndim=1, + negative_indices=False, mode='c'] angles, + int levels, + np.ndarray[dtype=np.uint32_t, ndim=4, + negative_indices=False, mode='c'] out + ): + """Perform co-occurnace matrix accumulation + + Parameters + ---------- + image : ndarray + Input image, which is converted to the uint8 data type. + distances : ndarray + List of pixel pair distance offsets. + angles : ndarray + List of pixel pair angles in radians. + levels : int + 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) + out : ndarray + On input a 4D array of zeros, and on output it contains + the results of the GLCM computation. + + """ + cdef: + np.int32_t a_inx, d_idx + np.int32_t r, c, rows, cols, row, col + np.int32_t i, j + + rows = image.shape[0] + cols = image.shape[1] + + for a_idx, angle in enumerate(angles): + for d_idx, distance in enumerate(distances): + for r in range(rows): + for c in range(cols): + i = image[r, c] + + # compute the location of the offset pixel + row = r + (sin(angle) * distance + 0.5) + col = c + (cos(angle) * distance + 0.5); + + # make sure the offset is within bounds + if row >= 0 and row < rows and \ + col >= 0 and col < cols: + j = image[row, col] + + if i >= 0 and i < levels and \ + j >= 0 and j < levels: + out[i, j, d_idx, a_idx] += 1 + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.nonecheck(False) +@cython.cdivision(True) +cdef _bilinear_interpolation(np.ndarray[double, ndim=2] image, + np.ndarray[double, ndim=2] coords, + np.ndarray[double, ndim=1] output, + double r0=0, double c0=0): + cdef double r, c, dr, dc + cdef int i, minr, minc, maxr, maxc + + for i in range(coords.shape[0]): + r = r0 + coords[i, 0] + c = c0 + coords[i, 1] + minr = floor(r) + minc = floor(c) + maxr = ceil(r) + maxc = ceil(c) + dr = r - minr + dc = c - minc + top = (1 - dc) * image[minr, minc] + dc * image[minr, maxc] + bottom = (1 - dc) * image[maxr, minc] + dc * image[maxr, maxc] + output[i] = (1 - dr) * top + dr * bottom + + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.nonecheck(False) +@cython.cdivision(True) +cdef int _bit_rotate_right(int value, int length): + """Cyclic bit shift to the right. + + Parameters + ---------- + value : int + integer value to shift + length : int + number of bits of integer + + """ + return (value >> 1) | ((value & 1) << (length - 1)) + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.nonecheck(False) +@cython.cdivision(True) +def _local_binary_pattern(np.ndarray[double, ndim=2] image, + int P, float R, int method=0): + # texture weights + cdef np.ndarray[int, ndim=1] weights = 2 ** np.arange(P, dtype='int32') + # local position of texture elements + rp = - R * np.sin(2 * np.pi * np.arange(P, dtype='double') / P) + cp = R * np.cos(2 * np.pi * np.arange(P, dtype='double') / P) + cdef np.ndarray[double, ndim=2] coords = np.round(np.vstack([rp, cp]).T, 5) + + # pre allocate arrays for computation + cdef np.ndarray[double, ndim=1] texture = np.zeros(P, 'double') + cdef np.ndarray[char, ndim=1] signed_texture = np.zeros(P, 'int8') + cdef np.ndarray[int, ndim=1] rotation_chain = np.zeros(P, 'int32') + + output_shape = (image.shape[0], image.shape[1]) + cdef np.ndarray[double, ndim=2] output = np.zeros(output_shape, 'double') + + cdef double lbp + cdef int r, c, changes, i + for r in range(image.shape[0]): + for c in range(image.shape[1]): + _bilinear_interpolation(image, coords, texture, r, c) + # signed / thresholded texture + for i in range(P): + if texture[i] - image[r, c] >= 0: + signed_texture[i] = 1 + else: + signed_texture[i] = 0 + + lbp = 0 + + # if method == 'uniform' or method == 'var': + if method == 2 or method == 3: + # determine number of 0 - 1 changes + changes = 0 + for i in range(P - 1): + changes += abs(signed_texture[i] - signed_texture[i + 1]) + + if changes <= 2: + for i in range(P): + lbp += signed_texture[i] + else: + lbp = P + 1 + + if method == 3: + var = np.var(texture) + if var != 0: + lbp /= var + else: + lbp = np.nan + else: + # method == 'default' + for i in range(P): + lbp += signed_texture[i] * weights[i] + + if method == 1: + # shift LBP P times to the right and get minimum value + rotation_chain[0] = lbp + for i in range(1, P): + rotation_chain[i] = \ + _bit_rotate_right(rotation_chain[i - 1], P) + lbp = rotation_chain[0] + for i in range(1, P): + lbp = min(lbp, rotation_chain[i]) + + output[r, c] = lbp + + return output diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index 2c50a592..dd765220 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -12,10 +12,10 @@ def configuration(parent_package='', top_path=None): config = Configuration('feature', parent_package, top_path) config.add_data_dir('tests') - cython(['_greycomatrix_cy.pyx'], working_path=base_path) + cython(['_texture.pyx'], working_path=base_path) cython(['_template.pyx'], working_path=base_path) - config.add_extension('_greycomatrix_cy', sources=['_greycomatrix_cy.c'], + config.add_extension('_texture', sources=['_texture.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_template', sources=['_template.c'], include_dirs=[get_numpy_include_dirs()]) diff --git a/skimage/feature/tests/test_texture.py b/skimage/feature/tests/test_texture.py index 02e9e98f..2c71cf9f 100644 --- a/skimage/feature/tests/test_texture.py +++ b/skimage/feature/tests/test_texture.py @@ -1,6 +1,5 @@ import numpy as np -from skimage.feature._texture import greycomatrix, greycoprops, \ - local_binary_pattern, bit_rotate_right +from skimage.feature import greycomatrix, greycoprops, local_binary_pattern class TestGLCM(): @@ -151,55 +150,53 @@ class TestLBP(): [ 8, 0, 159, 50, 255, 30], [167, 255, 63, 40, 128, 255], [ 0, 255, 30, 34, 255, 24], - [146, 241, 255, 0, 189, 126]], dtype=np.uint8) - - def test_bit_rotate_right(self): - np.testing.assert_equal(bit_rotate_right(11, 4), 13) + [146, 241, 255, 0, 189, 126]], dtype='double') def test_default(self): - lbp = local_binary_pattern(self.image, 8, 1) - ref = np.array([[ 0, 251, 0, 255, 96, 255], - [143, 0, 20, 153, 64, 56], - [238, 255, 12, 191, 0, 252], - [129, 0, 62, 159, 199, 0], - [255, 4, 255, 175, 0, 254], - [ 3, 5, 0, 255, 4, 24]]) + lbp = local_binary_pattern(self.image, 8, 1, 'default') + ref = np.array([[ 0., 251., 0., 255., 96., 255.], + [143., 0., 20., 153., 64., 184.], + [254., 255., 12., 191., 0., 255.], + [129., 64., 62., 159., 199., 0.], + [255., 4., 255., 175., 0., 255.], + [ 3., 5., 0., 223., 4., 24.]]) np.testing.assert_array_equal(lbp, ref) def test_ror(self): lbp = local_binary_pattern(self.image, 8, 1, 'ror') - ref = np.array([[ 0, 127, 0, 255, 3, 255], - [ 31, 0, 5, 51, 1, 7], - [119, 255, 3, 127, 0, 63], - [ 3, 0, 31, 63, 31, 0], - [255, 1, 255, 95, 0, 127], - [ 3, 5, 0, 255, 1, 3]]) + ref = np.array([[ 0., 127., 0., 255., 3., 255.], + [ 31., 0., 5., 51., 1., 23.], + [127., 255., 3., 127., 0., 255.], + [ 3., 1., 31., 63., 31., 0.], + [255., 1., 255., 95., 0., 255.], + [ 3., 5., 0., 255., 1., 11.]]) np.testing.assert_array_equal(lbp, ref) def test_uniform(self): lbp = local_binary_pattern(self.image, 8, 1, 'uniform') - ref = np.array([[0, 7, 0, 8, 2, 8], - [5, 0, 9, 9, 1, 3], - [9, 8, 2, 7, 0, 6], - [2, 0, 5, 6, 5, 0], - [8, 1, 8, 9, 0, 7], - [2, 9, 0, 8, 1, 2]]) + ref = np.array([[0., 7., 0., 8., 2., 8.], + [5., 0., 9., 9., 1., 9.], + [7., 8., 2., 7., 0., 8.], + [2., 1., 5., 6., 5., 0.], + [8., 1., 8., 9., 0., 8.], + [2., 9., 0., 8., 1., 2.]]) np.testing.assert_array_equal(lbp, ref) def test_var(self): lbp = local_binary_pattern(self.image, 8, 1, 'var') - ref = np.array([[0. , 0.00072786, 0. , 0.00115377, - 0.00032355, 0.00224467], - [0.00051758, 0. , 0.0026383 , 0.00163246, - 0.00027414, 0.00041124], - [0.00192834, 0.00130368, 0.00042095, 0.00171894, - 0. , 0.00063726], - [0.00023048, 0. , 0.00082291, 0.00225386, + print lbp + ref = np.array([[0. , 0.00072786, 0. , 0.00115376, + 0.00032355, 0.00252394], + [0.0005506 , 0. , 0.00263827, 0.00163246, + 0.00027414, 0.00144944], + [0.00195733, 0.00130368, 0.00042095, 0.00171893, + 0. , 0.00145748], + [0.00025994, 0.00019464, 0.00082291, 0.00225383, 0.00076696, 0. ], - [0.00097253, 0.00013236, 0.0009134 , 0.0014467 , - 0. , 0.00082472], - [0.00024701, 0.0012277 , 0. , 0.00109869, - 0.00015445, 0.00035881]]) + [0.00232001, 0.00013236, 0.0009134 , 0.0014467 , + 0. , 0.00179251], + [0.00027995, 0.0012277 , 0, 0.00109869, + 0.00015445, 0.00037256]]) np.testing.assert_array_almost_equal(lbp, ref) diff --git a/skimage/feature/_texture.py b/skimage/feature/texture.py similarity index 80% rename from skimage/feature/_texture.py rename to skimage/feature/texture.py index 1a456199..7eff0d30 100644 --- a/skimage/feature/_texture.py +++ b/skimage/feature/texture.py @@ -6,7 +6,7 @@ import math import numpy as np from scipy import ndimage -from ._greycomatrix import _glcm_loop +from ._texture import _glcm_loop, _local_binary_pattern def greycomatrix(image, distances, angles, levels=256, symmetric=False, @@ -227,20 +227,6 @@ def greycoprops(P, prop='contrast'): return results -def bit_rotate_right(value, length): - """Cyclic bit shift to the right. - - Parameters - ---------- - value : int - integer value to shift - length : int - number of bits of integer - - """ - return (value >> 1) | ((value & 1) << (length - 1)) - - def local_binary_pattern(image, P, R, method='default'): """Texture classification using gray scale and rotation invariant LBP (Local Binary Patterns). @@ -278,66 +264,13 @@ def local_binary_pattern(image, P, R, method='default'): http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/\ Articoliriferimento/LBP.pdf, 2002. """ - method = method.lower() - # texture weights - weights = 2 ** np.arange(P) - # local position of texture elements - rp = - R * np.sin(2 * math.pi * np.arange(P) / P) - cp = R * np.cos(2 * math.pi * np.arange(P) / P) - coords = np.vstack([rp, cp]) + math.ceil(R) - # maximum size of neighbourhood for filtering - max_size = 2 * math.ceil(R) + 1 - # center index of flattened neighbourhood - center_index = (max_size ** 2 - 1) / 2 - - if method == 'ror': - # allocate array for rotation invariance - rotation_chain = np.zeros(P, dtype='int') - - def compute_lbp(texture): - # subtract value of center pixel - texture -= texture[center_index] - # get texture elements using bilinear interpolation - texture = texture.reshape(max_size, max_size) - texture = ndimage.map_coordinates(texture, coords, order=1) - - # signed / thresholded texture - signed = texture.copy() - signed[signed >= 0] = 1 - signed[signed < 0] = 0 - - if method in ('uniform', 'var'): - # determine number of 0 - 1 changes - changes = np.sum(np.abs(np.diff(signed))) - - if changes <= 2: - lbp = np.sum(signed) - else: - lbp = P + 1 - - if method == 'var': - lbp /= np.var(texture) - else: - - # method == 'default' - lbp = np.sum(signed * weights) - - if method == 'ror': - # shift LBP P times to the right and get minimum value - rotation_chain[0] = lbp - for i in xrange(1, P): - rotation_chain[i] = \ - bit_rotate_right(rotation_chain[i - 1], P) - lbp = np.min(rotation_chain) - - return lbp - - dtype = 'int' - if method == 'var': - dtype = 'float' - output = np.zeros(image.shape, dtype) - - ndimage.generic_filter(image, compute_lbp, size=(max_size, max_size), - mode='constant', cval=0, output=output) + methods = { + 'default': 0, + 'ror': 1, + 'uniform': 2, + 'var': 3 + } + image = np.array(image, dtype='double', copy=True) + output = _local_binary_pattern(image, P, R, methods[method.lower()]) return output From bfa55789793a11e366289132e9ba0c33d71b8263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 6 Aug 2012 08:47:23 +0200 Subject: [PATCH 274/648] fix bilinear interpolation when position outside of image --- skimage/feature/_texture.pyx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index d79fa6a6..b97c6861 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -69,7 +69,7 @@ def _glcm_loop(np.ndarray[dtype=np.uint8_t, ndim=2, cdef _bilinear_interpolation(np.ndarray[double, ndim=2] image, np.ndarray[double, ndim=2] coords, np.ndarray[double, ndim=1] output, - double r0=0, double c0=0): + double r0=0, double c0=0, double cval=0): cdef double r, c, dr, dc cdef int i, minr, minc, maxr, maxc @@ -82,10 +82,15 @@ cdef _bilinear_interpolation(np.ndarray[double, ndim=2] image, maxc = ceil(c) dr = r - minr dc = c - minc - top = (1 - dc) * image[minr, minc] + dc * image[minr, maxc] - bottom = (1 - dc) * image[maxr, minc] + dc * image[maxr, maxc] - output[i] = (1 - dr) * top + dr * bottom - + if ( + minr < 0 or maxr >= image.shape[0] + or minc < 0 or maxc >= image.shape[1] + ): + output[i] = cval + else: + top = (1 - dc) * image[minr, minc] + dc * image[minr, maxc] + bottom = (1 - dc) * image[maxr, minc] + dc * image[maxr, maxc] + output[i] = (1 - dr) * top + dr * bottom @cython.boundscheck(False) From 55c3ec84e5bea8ab6d0c27667aff4c851bf3bfee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 8 Aug 2012 22:18:36 +0200 Subject: [PATCH 275/648] fix test cases for local binary pattern test results changed because border handling of bilinear interpolation changed --- skimage/feature/tests/test_texture.py | 54 +++++++++++++-------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/skimage/feature/tests/test_texture.py b/skimage/feature/tests/test_texture.py index 2c71cf9f..feeffaa2 100644 --- a/skimage/feature/tests/test_texture.py +++ b/skimage/feature/tests/test_texture.py @@ -154,49 +154,49 @@ class TestLBP(): def test_default(self): lbp = local_binary_pattern(self.image, 8, 1, 'default') - ref = np.array([[ 0., 251., 0., 255., 96., 255.], - [143., 0., 20., 153., 64., 184.], - [254., 255., 12., 191., 0., 255.], + ref = np.array([[ 0., 241., 0., 255., 96., 255.], + [135., 0., 20., 153., 64., 56.], + [198., 255., 12., 191., 0., 124.], [129., 64., 62., 159., 199., 0.], - [255., 4., 255., 175., 0., 255.], - [ 3., 5., 0., 223., 4., 24.]]) + [255., 4., 255., 175., 0., 124.], + [ 3., 5., 0., 255., 4., 24.]]) + print lbp np.testing.assert_array_equal(lbp, ref) def test_ror(self): lbp = local_binary_pattern(self.image, 8, 1, 'ror') - ref = np.array([[ 0., 127., 0., 255., 3., 255.], - [ 31., 0., 5., 51., 1., 23.], - [127., 255., 3., 127., 0., 255.], - [ 3., 1., 31., 63., 31., 0.], - [255., 1., 255., 95., 0., 255.], - [ 3., 5., 0., 255., 1., 11.]]) + ref = np.array([[ 0., 31., 0., 255., 3., 255.], + [ 15., 0., 5., 51., 1., 7.], + [ 27., 255., 3., 127., 0., 31.], + [ 3., 1., 31., 63., 31., 0.], + [255., 1., 255., 95., 0., 31.], + [ 3., 5., 0., 255., 1., 3.]]) np.testing.assert_array_equal(lbp, ref) def test_uniform(self): lbp = local_binary_pattern(self.image, 8, 1, 'uniform') - ref = np.array([[0., 7., 0., 8., 2., 8.], - [5., 0., 9., 9., 1., 9.], - [7., 8., 2., 7., 0., 8.], + ref = np.array([[0., 5., 0., 8., 2., 8.], + [4., 0., 9., 9., 1., 3.], + [9., 8., 2., 7., 0., 5.], [2., 1., 5., 6., 5., 0.], - [8., 1., 8., 9., 0., 8.], + [8., 1., 8., 9., 0., 5.], [2., 9., 0., 8., 1., 2.]]) np.testing.assert_array_equal(lbp, ref) def test_var(self): lbp = local_binary_pattern(self.image, 8, 1, 'var') - print lbp - ref = np.array([[0. , 0.00072786, 0. , 0.00115376, - 0.00032355, 0.00252394], - [0.0005506 , 0. , 0.00263827, 0.00163246, - 0.00027414, 0.00144944], - [0.00195733, 0.00130368, 0.00042095, 0.00171893, - 0. , 0.00145748], - [0.00025994, 0.00019464, 0.00082291, 0.00225383, + ref = np.array([[0. , 0.00039254, 0. , 0.00089309, + 0.00030782, 0.00203232], + [0.00037561, 0. , 0.00263827, 0.00163246, + 0.00027414, 0.00039593], + [0.00170876, 0.00130368, 0.00042095, 0.00171893, + 0. , 0.00044912], + [0.00021898, 0.00019464, 0.00082291, 0.00225383, 0.00076696, 0. ], - [0.00232001, 0.00013236, 0.0009134 , 0.0014467 , - 0. , 0.00179251], - [0.00027995, 0.0012277 , 0, 0.00109869, - 0.00015445, 0.00037256]]) + [0.00079791, 0.00013236, 0.0009134 , 0.0014467 , + 0. , 0.00046857], + [0.00022553, 0.00089319, 0. , 0.00089274, + 0.00013659, 0.00031981]]) np.testing.assert_array_almost_equal(lbp, ref) From 0fbbed525f1471dec9589a0fc118a3277c0df6b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 12 Aug 2012 10:37:48 +0200 Subject: [PATCH 276/648] improve doc string of local_binary_pattern function --- skimage/feature/texture.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index 7eff0d30..101f4a5a 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -228,12 +228,13 @@ def greycoprops(P, prop='contrast'): def local_binary_pattern(image, P, R, method='default'): - """Texture classification using gray scale and rotation invariant LBP - (Local Binary Patterns). + """Gray scale and rotation invariant LBP (Local Binary Patterns). + + LBP is an invariant descriptor that can be used for texture classification. Parameters ---------- - image : NxM array + image : (N, M) array graylevel image P : int number of circularly symmetric neighbour set points (quantization of the @@ -254,15 +255,16 @@ def local_binary_pattern(image, P, R, method='default'): Returns ------- - output : NxM array + output : (N, M) array LBP image References ---------- - Timo Ojala, Matti Pietikainen, Topi Maenpaa. Multiresolution Gray-Scale and - Rotation Invariant Texture Classification with Local Binary Patterns. - http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/\ - Articoliriferimento/LBP.pdf, 2002. + .. [1] Multiresolution Gray-Scale and Rotation Invariant Texture + Classification with Local Binary Patterns. + Timo Ojala, Matti Pietikainen, Topi Maenpaa. + http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/\ + Articoliriferimento/LBP.pdf, 2002. """ methods = { From 75c7926412791c7d2846f1df641406a968799c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 12 Aug 2012 10:45:55 +0200 Subject: [PATCH 277/648] use new subplots function in example script of LBP --- doc/examples/plot_local_binary_pattern.py | 35 +++++++++++------------ 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py index b00536a7..d961d136 100644 --- a/doc/examples/plot_local_binary_pattern.py +++ b/doc/examples/plot_local_binary_pattern.py @@ -24,6 +24,7 @@ from skimage import data METHOD = 'uniform' P = 16 R = 2 +matplotlib.rcParams['font.size'] = 9 def kullback_leibler_divergence(p, q): @@ -64,24 +65,20 @@ print match(refs, nd.rotate(brick, angle=70, reshape=False)) print match(refs, nd.rotate(grass, angle=145, reshape=False)) # plot histograms of LBP of textures -matplotlib.rcParams['font.size'] = 9 -plt.figure(figsize=(9, 6)) -plt.subplot(231) -plt.imshow(brick) -plt.axis('off') +fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(nrows=2, ncols=3, + figsize=(9, 6)) plt.gray() -plt.subplot(234) -plt.hist(refs['brick'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) -plt.subplot(232) -plt.imshow(grass) -plt.axis('off') -plt.gray() -plt.subplot(235) -plt.hist(refs['grass'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) -plt.subplot(233) -plt.imshow(wall) -plt.axis('off') -plt.gray() -plt.subplot(236) -plt.hist(refs['wall'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) + +ax1.imshow(brick) +ax1.axis('off') +ax4.hist(refs['brick'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) + +ax2.imshow(grass) +ax2.axis('off') +ax5.hist(refs['grass'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) + +ax3.imshow(wall) +ax3.axis('off') +ax6.hist(refs['wall'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) + plt.show() From 0ca7933a7d1cf573d12fcf15f72b9f03afeb84f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 16 Aug 2012 19:08:13 +0200 Subject: [PATCH 278/648] Share bilinear interpolation function for other code Make bilinear_interpolation function callable by other cython code and change fast_homography to use this function. Also, improve performance of some functions by making them inline and fix broken test of fast_homography. --- skimage/transform/_project.pxd | 12 +++ skimage/transform/_project.pyx | 112 +++++++++++--------------- skimage/transform/tests/test_warps.py | 23 +++--- 3 files changed, 72 insertions(+), 75 deletions(-) create mode 100644 skimage/transform/_project.pxd diff --git a/skimage/transform/_project.pxd b/skimage/transform/_project.pxd new file mode 100644 index 00000000..36a32d2d --- /dev/null +++ b/skimage/transform/_project.pxd @@ -0,0 +1,12 @@ +cimport numpy as np +import numpy as np + + +cdef inline double bilinear_interpolation(double* image, int rows, int cols, + double r, double c, char mode, + double cval=*) + +cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, + char mode, double cval=*) + +cdef inline int coord_map(int dim, int coord, char mode) \ No newline at end of file diff --git a/skimage/transform/_project.pyx b/skimage/transform/_project.pyx index 734d6bff..963670c6 100644 --- a/skimage/transform/_project.pyx +++ b/skimage/transform/_project.pyx @@ -1,38 +1,50 @@ -#cython: cdivison=True boundscheck=False +#cython: cdivison=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False -__all__ = ['homography'] - -cimport cython cimport numpy as np - import numpy as np -import cython - from cython.operator import dereference +from libc.math cimport ceil, floor -np.import_array() -cdef extern from "math.h": - double floor(double) - double fmod(double, double) +cdef inline double bilinear_interpolation(double* image, int rows, int cols, + double r, double c, char mode, + double cval=0): + cdef double dr, dc + cdef int minr, minc, maxr, maxc -cdef double get_pixel(double *image, int rows, int cols, - int r, int c, char mode, double cval=0): + minr = floor(r) + minc = floor(c) + maxr = ceil(r) + maxc = ceil(c) + dr = r - minr + dc = c - minc + top = (1 - dc) * get_pixel(image, rows, cols, minr, minc, mode, cval) \ + + dc * get_pixel(image, rows, cols, minr, maxc, mode, cval) + bottom = (1 - dc) * get_pixel(image, rows, cols, maxr, minc, mode, cval) \ + + dc * get_pixel(image, rows, cols, maxr, maxc, mode, cval) + return (1 - dr) * top + dr * bottom + + +cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, + char mode, double cval=0): """Get a pixel from the image, taking wrapping mode into consideration. Parameters ---------- - image : *double + image : array of dtype double Input image. - rows, cols : int - Dimensions of image. + rows, cols: int + Shape of image. r, c : int Position at which to get the pixel. mode : {'C', 'W', 'M'} - Wrapping mode. Constant, Wrap or Mirror. + Wrapping mode. Constant, Wrap or Mirror. cval : double - Constant value to use for mode constant. - + Constant value to use for constant mode. + """ if mode == 'C': if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): @@ -40,13 +52,13 @@ cdef double get_pixel(double *image, int rows, int cols, else: return image[r * cols + c] else: - return image[coord_map(rows, r, mode) * cols + - coord_map(cols, c, mode)] + return image[coord_map(rows, r, mode) * cols + coord_map(cols, c, mode)] -cdef int coord_map(int dim, int coord, char mode): + +cdef inline int coord_map(int dim, int coord, char mode): """ - Wrap a coordinate, according to a given dimension and mode. - + Wrap a coordinate, according to a given mode. + Parameters ---------- dim : int @@ -56,7 +68,7 @@ cdef int coord_map(int dim, int coord, char mode): mode : {'W', 'M'} Whether to wrap or mirror the coordinate if it falls outside [0, dim). - + """ dim = dim - 1 if mode == 'M': # mirror @@ -79,7 +91,8 @@ cdef int coord_map(int dim, int coord, char mode): return coord -cdef tf(double x, double y, double* H, double *x_, double *y_): + +cdef inline tf(double x, double y, double* H, double *x_, double *y_): """Apply a homography to a coordinate. Parameters @@ -98,18 +111,15 @@ cdef tf(double x, double y, double* H, double *x_, double *y_): yy = H[3] * x + H[4] * y + H[5] zz = H[6] * x + H[7] * y + H[8] - xx = xx / zz - yy = yy / zz + x_[0] = xx / zz + y_[0] = yy / zz - x_[0] = xx - y_[0] = yy -@cython.boundscheck(False) def homography(np.ndarray image, np.ndarray H, output_shape=None, mode='constant', double cval=0): """ Projective transformation (homography). - + Perform a projective transformation (homography) of a floating point image, using bi-linear interpolation. @@ -140,8 +150,6 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, Transformation matrix H that defines the homography. output_shape : tuple (rows, cols) Shape of the output image generated. - order : int - Order of splines used in interpolation. mode : {'constant', 'mirror', 'wrap'} How to handle values outside the image borders. cval : string @@ -150,8 +158,7 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, """ - cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] img = \ - np.ascontiguousarray(image, dtype=np.double) + cdef np.ndarray[dtype=np.double_t, ndim=2] img = image.astype(np.double) cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] M = \ np.ascontiguousarray(np.linalg.inv(H)) @@ -165,7 +172,6 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, elif mode == 'mirror': mode_c = ord('M') - cdef int out_r, out_c, columns, rows if output_shape is None: out_r = img.shape[0] out_c = img.shape[1] @@ -173,37 +179,17 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, out_r = output_shape[0] out_c = output_shape[1] - rows = img.shape[0] - columns = img.shape[1] - cdef np.ndarray[dtype=np.double_t, ndim=2] out = \ np.zeros((out_r, out_c), dtype=np.double) - - cdef int tfr, tfc, r_int, c_int - cdef double y0, y1, y2, y3 - cdef double r, c, z, t, u + + cdef int tfr, tfc + cdef double r, c + cdef int rows = img.shape[0] + cdef int cols = img.shape[1] for tfr in range(out_r): for tfc in range(out_c): tf(tfc, tfr, M.data, &c, &r) - r_int = floor(r) - c_int = floor(c) - - t = r - r_int - u = c - c_int - - y0 = get_pixel(img.data, rows, columns, - r_int, c_int, mode_c) - y1 = get_pixel(img.data, rows, columns, - r_int + 1, c_int, mode_c) - y2 = get_pixel(img.data, rows, columns, - r_int + 1, c_int + 1, mode_c) - y3 = get_pixel(img.data, rows, columns, - r_int, c_int + 1, mode_c) - - out[tfr, tfc] = \ - (1 - t) * (1 - u) * y0 + \ - t * (1 - u) * y1 + \ - t * u * y2 + (1 - t) * u * y3; + out[tfr, tfc] = bilinear_interpolation(img.data, rows, cols, r, c, mode_c) return out diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 7c2e52f2..f35b7f57 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -2,7 +2,7 @@ from numpy.testing import assert_array_almost_equal, run_module_suite import numpy as np from skimage.transform import (warp, homography, fast_homography, - SimilarityTransform) + SimilarityTransform, ProjectiveTransform) from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray @@ -34,7 +34,7 @@ def test_homography(): def test_fast_homography(): - img = rgb2gray(data.lena()).astype(np.uint8) + img = rgb2gray(data.lena()) img = img[:, :100] theta = np.deg2rad(30) @@ -49,20 +49,19 @@ def test_fast_homography(): H[:2, 2] = [tx, ty] for mode in ('constant', 'mirror', 'wrap'): - p0 = homography(img, H, mode=mode, order=1) + p0 = warp(img, ProjectiveTransform(H).inverse, mode=mode, order=1) p1 = fast_homography(img, H, mode=mode) - p1 = np.round(p1) - ## import matplotlib.pyplot as plt - ## f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4) - ## ax0.imshow(img) - ## ax1.imshow(p0, cmap=plt.cm.gray) - ## ax2.imshow(p1, cmap=plt.cm.gray) - ## ax3.imshow(np.abs(p0 - p1), cmap=plt.cm.gray) - ## plt.show() + # import matplotlib.pyplot as plt + # f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4) + # ax0.imshow(img) + # ax1.imshow(p0, cmap=plt.cm.gray) + # ax2.imshow(p1, cmap=plt.cm.gray) + # ax3.imshow(np.abs(p0 - p1), cmap=plt.cm.gray) + # plt.show() d = np.mean(np.abs(p0 - p1)) - assert d < 0.2 + assert d < 0.001 def test_swirl(): From 70b7745a3456d2e1e1e19b7157f2e19a1825142d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 16 Aug 2012 19:12:14 +0200 Subject: [PATCH 279/648] Use shared bilinear interpolation function and improve performance. --- skimage/feature/_texture.pyx | 57 ++++++-------------------- skimage/feature/setup.py | 3 +- skimage/feature/tests/test_texture.py | 59 +++++++++++++-------------- 3 files changed, 44 insertions(+), 75 deletions(-) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index b97c6861..44d6e2d6 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -1,10 +1,13 @@ +#cython: cdivison=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False import numpy as np cimport numpy as np -cimport cython -from libc.math cimport sin, cos, abs, ceil, floor +from libc.math cimport sin, cos, abs +from skimage.transform._project cimport bilinear_interpolation -@cython.boundscheck(False) def _glcm_loop(np.ndarray[dtype=np.uint8_t, ndim=2, negative_indices=False, mode='c'] image, np.ndarray[dtype=np.float64_t, ndim=1, @@ -62,42 +65,7 @@ def _glcm_loop(np.ndarray[dtype=np.uint8_t, ndim=2, out[i, j, d_idx, a_idx] += 1 -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.nonecheck(False) -@cython.cdivision(True) -cdef _bilinear_interpolation(np.ndarray[double, ndim=2] image, - np.ndarray[double, ndim=2] coords, - np.ndarray[double, ndim=1] output, - double r0=0, double c0=0, double cval=0): - cdef double r, c, dr, dc - cdef int i, minr, minc, maxr, maxc - - for i in range(coords.shape[0]): - r = r0 + coords[i, 0] - c = c0 + coords[i, 1] - minr = floor(r) - minc = floor(c) - maxr = ceil(r) - maxc = ceil(c) - dr = r - minr - dc = c - minc - if ( - minr < 0 or maxr >= image.shape[0] - or minc < 0 or maxc >= image.shape[1] - ): - output[i] = cval - else: - top = (1 - dc) * image[minr, minc] + dc * image[minr, maxc] - bottom = (1 - dc) * image[maxr, minc] + dc * image[maxr, maxc] - output[i] = (1 - dr) * top + dr * bottom - - -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.nonecheck(False) -@cython.cdivision(True) -cdef int _bit_rotate_right(int value, int length): +cdef inline int _bit_rotate_right(int value, int length): """Cyclic bit shift to the right. Parameters @@ -111,10 +79,6 @@ cdef int _bit_rotate_right(int value, int length): return (value >> 1) | ((value & 1) << (length - 1)) -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.nonecheck(False) -@cython.cdivision(True) def _local_binary_pattern(np.ndarray[double, ndim=2] image, int P, float R, int method=0): # texture weights @@ -132,11 +96,16 @@ def _local_binary_pattern(np.ndarray[double, ndim=2] image, output_shape = (image.shape[0], image.shape[1]) cdef np.ndarray[double, ndim=2] output = np.zeros(output_shape, 'double') + cdef int rows = image.shape[0] + cdef int cols = image.shape[1] + cdef double lbp cdef int r, c, changes, i for r in range(image.shape[0]): for c in range(image.shape[1]): - _bilinear_interpolation(image, coords, texture, r, c) + for i in range(P): + texture[i] = bilinear_interpolation(image.data, + rows, cols, r + coords[i, 0], c + coords[i, 1], 'C') # signed / thresholded texture for i in range(P): if texture[i] - image[r, c] >= 0: diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index dd765220..0b0b80bd 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -16,7 +16,8 @@ def configuration(parent_package='', top_path=None): cython(['_template.pyx'], working_path=base_path) config.add_extension('_texture', sources=['_texture.c'], - include_dirs=[get_numpy_include_dirs()]) + include_dirs=[get_numpy_include_dirs(), + '../transform']) config.add_extension('_template', sources=['_template.c'], include_dirs=[get_numpy_include_dirs()]) diff --git a/skimage/feature/tests/test_texture.py b/skimage/feature/tests/test_texture.py index feeffaa2..d48a14f7 100644 --- a/skimage/feature/tests/test_texture.py +++ b/skimage/feature/tests/test_texture.py @@ -154,49 +154,48 @@ class TestLBP(): def test_default(self): lbp = local_binary_pattern(self.image, 8, 1, 'default') - ref = np.array([[ 0., 241., 0., 255., 96., 255.], - [135., 0., 20., 153., 64., 56.], - [198., 255., 12., 191., 0., 124.], - [129., 64., 62., 159., 199., 0.], - [255., 4., 255., 175., 0., 124.], - [ 3., 5., 0., 255., 4., 24.]]) - print lbp + ref = np.array([[ 0, 251, 0, 255, 96, 255], + [143, 0, 20, 153, 64, 56], + [238, 255, 12, 191, 0, 252], + [129, 64., 62, 159, 199, 0], + [255, 4, 255, 175, 0, 254], + [ 3, 5, 0, 255, 4, 24]]) np.testing.assert_array_equal(lbp, ref) def test_ror(self): lbp = local_binary_pattern(self.image, 8, 1, 'ror') - ref = np.array([[ 0., 31., 0., 255., 3., 255.], - [ 15., 0., 5., 51., 1., 7.], - [ 27., 255., 3., 127., 0., 31.], - [ 3., 1., 31., 63., 31., 0.], - [255., 1., 255., 95., 0., 31.], - [ 3., 5., 0., 255., 1., 3.]]) + ref = np.array([[ 0, 127, 0, 255, 3, 255], + [ 31, 0, 5, 51, 1, 7], + [119, 255, 3, 127, 0, 63], + [ 3, 1, 31, 63, 31, 0], + [255, 1, 255, 95, 0, 127], + [ 3, 5, 0, 255, 1, 3]]) np.testing.assert_array_equal(lbp, ref) def test_uniform(self): lbp = local_binary_pattern(self.image, 8, 1, 'uniform') - ref = np.array([[0., 5., 0., 8., 2., 8.], - [4., 0., 9., 9., 1., 3.], - [9., 8., 2., 7., 0., 5.], - [2., 1., 5., 6., 5., 0.], - [8., 1., 8., 9., 0., 5.], - [2., 9., 0., 8., 1., 2.]]) + ref = np.array([[0, 7, 0, 8, 2, 8], + [5, 0, 9, 9, 1, 3], + [9, 8, 2, 7, 0, 6], + [2, 1, 5, 6, 5, 0], + [8, 1, 8, 9, 0, 7], + [2, 9, 0, 8, 1, 2]]) np.testing.assert_array_equal(lbp, ref) def test_var(self): lbp = local_binary_pattern(self.image, 8, 1, 'var') - ref = np.array([[0. , 0.00039254, 0. , 0.00089309, - 0.00030782, 0.00203232], - [0.00037561, 0. , 0.00263827, 0.00163246, - 0.00027414, 0.00039593], - [0.00170876, 0.00130368, 0.00042095, 0.00171893, - 0. , 0.00044912], - [0.00021898, 0.00019464, 0.00082291, 0.00225383, + ref = np.array([[0. , 0.00072786, 0. , 0.00115377, + 0.00032355, 0.00224467], + [0.00051758, 0. , 0.0026383 , 0.00163246, + 0.00027414, 0.00041124], + [0.00192834, 0.00130368, 0.00042095, 0.00171894, + 0. , 0.00063726], + [0.00023048, 0.00019464 , 0.00082291, 0.00225386, 0.00076696, 0. ], - [0.00079791, 0.00013236, 0.0009134 , 0.0014467 , - 0. , 0.00046857], - [0.00022553, 0.00089319, 0. , 0.00089274, - 0.00013659, 0.00031981]]) + [0.00097253, 0.00013236, 0.0009134 , 0.0014467 , + 0. , 0.00082472], + [0.00024701, 0.0012277 , 0. , 0.00109869, + 0.00015445, 0.00035881]]) np.testing.assert_array_almost_equal(lbp, ref) From 6cefa4ad6242736b66ffd667c8e2210d2082fa7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 16 Aug 2012 19:13:15 +0200 Subject: [PATCH 280/648] Wrap column --- skimage/transform/_project.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/transform/_project.pyx b/skimage/transform/_project.pyx index 963670c6..282411eb 100644 --- a/skimage/transform/_project.pyx +++ b/skimage/transform/_project.pyx @@ -190,6 +190,7 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, for tfr in range(out_r): for tfc in range(out_c): tf(tfc, tfr, M.data, &c, &r) - out[tfr, tfc] = bilinear_interpolation(img.data, rows, cols, r, c, mode_c) + out[tfr, tfc] = bilinear_interpolation(img.data, rows, + cols, r, c, mode_c) return out From b5e2b01620855da10722afc2021f08fa1a4c68ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 16 Aug 2012 19:16:17 +0200 Subject: [PATCH 281/648] Add doc string to bilinear_interpolation --- skimage/transform/_project.pyx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/skimage/transform/_project.pyx b/skimage/transform/_project.pyx index 282411eb..245f63cf 100644 --- a/skimage/transform/_project.pyx +++ b/skimage/transform/_project.pyx @@ -12,6 +12,22 @@ from libc.math cimport ceil, floor cdef inline double bilinear_interpolation(double* image, int rows, int cols, double r, double c, char mode, double cval=0): + """Bilinear interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols: int + Shape of image. + r, c : int + Position at which to interpolate. + mode : {'C', 'W', 'M'} + Wrapping mode. Constant, Wrap or Mirror. + cval : double + Constant value to use for constant mode. + + """ cdef double dr, dc cdef int minr, minc, maxr, maxc @@ -34,7 +50,7 @@ cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, Parameters ---------- - image : array of dtype double + image : double array Input image. rows, cols: int Shape of image. From 53472b8e075e786c586b9c537453cf3992396e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 16 Aug 2012 19:20:48 +0200 Subject: [PATCH 282/648] Rename matrix transform function and remove from pxd --- skimage/transform/_project.pxd | 5 ----- skimage/transform/_project.pyx | 5 +++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/skimage/transform/_project.pxd b/skimage/transform/_project.pxd index 36a32d2d..dee51026 100644 --- a/skimage/transform/_project.pxd +++ b/skimage/transform/_project.pxd @@ -5,8 +5,3 @@ import numpy as np cdef inline double bilinear_interpolation(double* image, int rows, int cols, double r, double c, char mode, double cval=*) - -cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, - char mode, double cval=*) - -cdef inline int coord_map(int dim, int coord, char mode) \ No newline at end of file diff --git a/skimage/transform/_project.pyx b/skimage/transform/_project.pyx index 245f63cf..3f31f229 100644 --- a/skimage/transform/_project.pyx +++ b/skimage/transform/_project.pyx @@ -108,7 +108,8 @@ cdef inline int coord_map(int dim, int coord, char mode): return coord -cdef inline tf(double x, double y, double* H, double *x_, double *y_): +cdef inline _matrix_transform(double x, double y, double* H, double *x_, + double *y_): """Apply a homography to a coordinate. Parameters @@ -205,7 +206,7 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, for tfr in range(out_r): for tfc in range(out_c): - tf(tfc, tfr, M.data, &c, &r) + _matrix_transform(tfc, tfr, M.data, &c, &r) out[tfr, tfc] = bilinear_interpolation(img.data, rows, cols, r, c, mode_c) From f11b76e709bae11ac9c5050919e6d95f92f06b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 20 Aug 2012 18:45:21 +0200 Subject: [PATCH 283/648] Add full-stop at end of short doc string description --- skimage/feature/_texture.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 44d6e2d6..63a30b04 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -18,7 +18,7 @@ def _glcm_loop(np.ndarray[dtype=np.uint8_t, ndim=2, np.ndarray[dtype=np.uint32_t, ndim=4, negative_indices=False, mode='c'] out ): - """Perform co-occurnace matrix accumulation + """Perform co-occurnace matrix accumulation. Parameters ---------- From 777bb59834b2d7b4cabddd70689e9ac9162c2f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 20 Aug 2012 18:53:02 +0200 Subject: [PATCH 284/648] Remove unused imports and wrap text --- doc/examples/plot_local_binary_pattern.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py index d961d136..7963bff7 100644 --- a/doc/examples/plot_local_binary_pattern.py +++ b/doc/examples/plot_local_binary_pattern.py @@ -3,20 +3,17 @@ Local Binary Pattern for texture classification =============================================== -In this example, we will see how to classify textures based on LBP (Local Binary -Pattern). The histogram of the LBP result is a good measure to classify +In this example, we will see how to classify textures based on LBP (Local +Binary Pattern). The histogram of the LBP result is a good measure to classify textures. For simplicity the histogram distributions are then tested against each other using the Kullback-Leibler-Divergence. """ -import os -import glob import numpy as np import matplotlib import matplotlib.pyplot as plt import scipy.ndimage as nd import skimage.feature as ft -from skimage.io import imread from skimage import data From 37f5ab47a6883157b04419d2f057e5c2974bfb0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 20 Aug 2012 18:53:55 +0200 Subject: [PATCH 285/648] Fix typo in doc string --- skimage/feature/_texture.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 63a30b04..58077167 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -18,7 +18,7 @@ def _glcm_loop(np.ndarray[dtype=np.uint8_t, ndim=2, np.ndarray[dtype=np.uint32_t, ndim=4, negative_indices=False, mode='c'] out ): - """Perform co-occurnace matrix accumulation. + """Perform co-occurrence matrix accumulation. Parameters ---------- From 36b22d7819c11b632cb2f91c93e50a8960c04cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 20 Aug 2012 19:12:00 +0200 Subject: [PATCH 286/648] Use numpy dtype objects instead of strings --- skimage/feature/_texture.pyx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 58077167..c8410fe8 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -82,19 +82,19 @@ cdef inline int _bit_rotate_right(int value, int length): def _local_binary_pattern(np.ndarray[double, ndim=2] image, int P, float R, int method=0): # texture weights - cdef np.ndarray[int, ndim=1] weights = 2 ** np.arange(P, dtype='int32') + cdef np.ndarray[int, ndim=1] weights = 2 ** np.arange(P, dtype=np.int32) # local position of texture elements - rp = - R * np.sin(2 * np.pi * np.arange(P, dtype='double') / P) - cp = R * np.cos(2 * np.pi * np.arange(P, dtype='double') / P) + rp = - R * np.sin(2 * np.pi * np.arange(P, dtype=np.double) / P) + cp = R * np.cos(2 * np.pi * np.arange(P, dtype=np.double) / P) cdef np.ndarray[double, ndim=2] coords = np.round(np.vstack([rp, cp]).T, 5) # pre allocate arrays for computation - cdef np.ndarray[double, ndim=1] texture = np.zeros(P, 'double') - cdef np.ndarray[char, ndim=1] signed_texture = np.zeros(P, 'int8') - cdef np.ndarray[int, ndim=1] rotation_chain = np.zeros(P, 'int32') + cdef np.ndarray[double, ndim=1] texture = np.zeros(P, np.double) + cdef np.ndarray[char, ndim=1] signed_texture = np.zeros(P, np.int8) + cdef np.ndarray[int, ndim=1] rotation_chain = np.zeros(P, np.int32) output_shape = (image.shape[0], image.shape[1]) - cdef np.ndarray[double, ndim=2] output = np.zeros(output_shape, 'double') + cdef np.ndarray[double, ndim=2] output = np.zeros(output_shape, np.double) cdef int rows = image.shape[0] cdef int cols = image.shape[1] From 2b7cb5f63002659dc46c8fdbea92065fcf809113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 20 Aug 2012 19:29:06 +0200 Subject: [PATCH 287/648] Make different methods of LBP more readable in Cython code --- skimage/feature/_texture.pyx | 9 +++++---- skimage/feature/texture.py | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index c8410fe8..e5d8a2ab 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -80,7 +80,7 @@ cdef inline int _bit_rotate_right(int value, int length): def _local_binary_pattern(np.ndarray[double, ndim=2] image, - int P, float R, int method=0): + int P, float R, char method='D'): # texture weights cdef np.ndarray[int, ndim=1] weights = 2 ** np.arange(P, dtype=np.int32) # local position of texture elements @@ -116,7 +116,7 @@ def _local_binary_pattern(np.ndarray[double, ndim=2] image, lbp = 0 # if method == 'uniform' or method == 'var': - if method == 2 or method == 3: + if method == 'U' or method == 'V': # determine number of 0 - 1 changes changes = 0 for i in range(P - 1): @@ -128,7 +128,7 @@ def _local_binary_pattern(np.ndarray[double, ndim=2] image, else: lbp = P + 1 - if method == 3: + if method == 'V': var = np.var(texture) if var != 0: lbp /= var @@ -139,7 +139,8 @@ def _local_binary_pattern(np.ndarray[double, ndim=2] image, for i in range(P): lbp += signed_texture[i] * weights[i] - if method == 1: + # method == 'ror' + if method == 'R': # shift LBP P times to the right and get minimum value rotation_chain[0] = lbp for i in range(1, P): diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index 101f4a5a..aa970d18 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -235,14 +235,14 @@ def local_binary_pattern(image, P, R, method='default'): Parameters ---------- image : (N, M) array - graylevel image + Graylevel image. P : int - number of circularly symmetric neighbour set points (quantization of the - angular space) + Number of circularly symmetric neighbour set points (quantization of the + angular space). R : float - radius of circle (spatial resolution of the operator) - method : {'default', 'ror', 'uniform', 'var'} - method to determine the pattern:: + Radius of circle (spatial resolution of the operator). + method : {'D', 'R', 'U', 'V'} + Method to determine the pattern:: * 'default': original local binary pattern which is gray scale but not rotation invariant. * 'ror': extension of default implementation which is gray scale and @@ -256,7 +256,7 @@ def local_binary_pattern(image, P, R, method='default'): Returns ------- output : (N, M) array - LBP image + LBP image. References ---------- @@ -268,10 +268,10 @@ def local_binary_pattern(image, P, R, method='default'): """ methods = { - 'default': 0, - 'ror': 1, - 'uniform': 2, - 'var': 3 + 'default': ord('D'), + 'ror': ord('R'), + 'uniform': ord('U'), + 'var': ord('V') } image = np.array(image, dtype='double', copy=True) output = _local_binary_pattern(image, P, R, methods[method.lower()]) From 321e3aefa8342d3b3f633fd4c1f72c1b3013d081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 20 Aug 2012 19:29:27 +0200 Subject: [PATCH 288/648] Add doc string to Cython version of LBP --- skimage/feature/_texture.pyx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index e5d8a2ab..ca51e177 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -81,6 +81,32 @@ cdef inline int _bit_rotate_right(int value, int length): def _local_binary_pattern(np.ndarray[double, ndim=2] image, int P, float R, char method='D'): + """Gray scale and rotation invariant LBP (Local Binary Patterns). + + LBP is an invariant descriptor that can be used for texture classification. + + Parameters + ---------- + image : (N, M) double array + Graylevel image. + P : int + Number of circularly symmetric neighbour set points (quantization of the + angular space). + R : float + Radius of circle (spatial resolution of the operator). + method : {'D', 'R', 'U', 'V'} + Method to determine the pattern:: + * 'D': 'default' + * 'R': 'ror' + * 'U': 'uniform' + * 'V': 'var' + + Returns + ------- + output : (N, M) array + LBP image. + """ + # texture weights cdef np.ndarray[int, ndim=1] weights = 2 ** np.arange(P, dtype=np.int32) # local position of texture elements From 70a69eb43756bc94b3cd1e86dd845e16dd93439b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 20 Aug 2012 19:56:17 +0200 Subject: [PATCH 289/648] Improve printed output of example --- doc/examples/plot_local_binary_pattern.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py index 7963bff7..95ece14e 100644 --- a/doc/examples/plot_local_binary_pattern.py +++ b/doc/examples/plot_local_binary_pattern.py @@ -57,8 +57,12 @@ refs = { } # classify rotated textures +print 'Rotated images matched against references using LBP:' +print 'original: brick, rotated: 30deg, match result:', print match(refs, nd.rotate(brick, angle=30, reshape=False)) +print 'original: brick, rotated: 70deg, match result:', print match(refs, nd.rotate(brick, angle=70, reshape=False)) +print 'original: grass, rotated: 145deg, match result:', print match(refs, nd.rotate(grass, angle=145, reshape=False)) # plot histograms of LBP of textures From 26a84fbb941621afa7212ad15b34335715d1a0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 20 Aug 2012 20:07:44 +0200 Subject: [PATCH 290/648] Add plot labels --- doc/examples/plot_local_binary_pattern.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py index 95ece14e..85fd5b95 100644 --- a/doc/examples/plot_local_binary_pattern.py +++ b/doc/examples/plot_local_binary_pattern.py @@ -73,10 +73,12 @@ plt.gray() ax1.imshow(brick) ax1.axis('off') ax4.hist(refs['brick'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +ax4.set_ylabel('Percentage') ax2.imshow(grass) ax2.axis('off') ax5.hist(refs['grass'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +ax5.set_xlabel('Uniform LBP values') ax3.imshow(wall) ax3.axis('off') From 014b4905784f50fd13111ca8528fade9be4bd767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 20 Aug 2012 22:50:35 +0200 Subject: [PATCH 291/648] Fix import bug due to rebase --- skimage/feature/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 4acb678e..1597e9a8 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -1,6 +1,4 @@ from ._hog import hog -from ._greycomatrix import greycomatrix, greycoprops -from .hog import hog from .texture import greycomatrix, greycoprops, local_binary_pattern from .peak import peak_local_max from ._harris import harris From d7d1faec4689faf803c835c6a512a067a272fdbb Mon Sep 17 00:00:00 2001 From: Neil Yager Date: Mon, 20 Aug 2012 21:56:10 +0100 Subject: [PATCH 292/648] Fix bug in skeletonize LUT --- skimage/data/bw_text_skeleton.npy | Bin 171908 -> 171908 bytes skimage/morphology/_skeletonize.py | 4 ++-- skimage/morphology/tests/test_skeletonize.py | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/skimage/data/bw_text_skeleton.npy b/skimage/data/bw_text_skeleton.npy index 9492cb647a5c5b9401c3526d4e9632ad416cba04..2933c48462db42b4eaa34a3832337fac75c9321c 100644 GIT binary patch delta 1410 zcmb7EU1(cn7~XqiS(7H`oVIHlrA@Q`jR{-NYF$bjZRkdacC{H!EZU1RL1k9DxYD2? z_KfX9bgfNa=sSi=rZ7|_WDR=2F%}VJ7YYF3HrUjg+%I}nEWu*~S15;Op>6-wR5d|c|TmVAy{GWNh- zMXIlzQ+>*MM3yHgC<7Mx+4?%jWlJxl`u^Div#Y=xoPYHjcBa0Ql@=|q&;z#;-3Pf;N?9oe4>FSgVqEhBbrV zt88OCy;Wunt%`4~)}q>w6mOEw^M`#L?T-|0On>j^se^S~JozX;e#uyJuC*cEFz^I$ zgo_t$%Uik@&e%cRpInn4`ndQRtu@oMCR6r@Ha0U^NPHUJ>Pdn;nfunORhPs^s3NG9ToZr)zlr_t2t7)t*d}9Z0;Au94KwWGuw1czHUAdqh%8Sf1xOS25U3 zn__oRqpF0X`FLTj^4xC3zW~I59e}H7(wMP(agHpWr5S9t3uhB(1;W6GZnlXge5&Pu zl+)znflEFa|C^qNGJ@!S$)6(}jyFoFg|1^9a`6SU1lR?i{aVIWtq)=ph%>gT zbW*-Xt(+Lpr5x7ujIGLPMLAVm_ihs>yVu9?n73awxn14`Pp(%LFVXkOWGmk+#U&if zcs0L0q_g!0G@WF)P^b1_I8VWOA8>%3k$NfqLIE!Sb_xPTZ1t-9VASuTs4kDjYDk+j zy|mEt|AqDIqY-N-b{PGE9#MJDgB2t7oVXfC9>pRxwqw8u@VR2q_Du#Wu~8_Z>(TCh z4yhBJ5Y7SkmHvySSfDyq*4_`*zR^?Vn!Vr4h1YSf`_RUCN|Pl_D5kiun@hLuy;rde z?G(fjPG0m$wvAN6GY`-f;*;s^?m1JNRy2BAz1C0gFD*UjS_ie8Bu+@fvhEn5P_3KT IuN@fw7xyMCR{#J2 delta 1328 zcmb_cO=w(I6!yHf$xLQ4@8xe2Z89C&PKDMqLx?scqaD)_C^2TzHg?oNuP$2JruHEw z8?kw6Ko@P2mxCUtMMp(Y>d$i?GGGfGannT?v&h0t$?OV7BoZhU7xla8ObuBnxcGSQ zedpYJ?)RN@-|UUL*&B7ex;Vx^ftLY5?7}i7S{seeL=O0sQ@oe zg+ax{&coj7;<*)*t!YbCf?9DxljB~>pIdpU(^a=obe`O0jLh{qA)Gkh*!t6~2aQ>*2@(`fL z!UZ!y9Z6RmtN0MXx-(dR6wi~|vlh1Qo1DHm3gR3viQo}DPi+oP{{O+0l$U6qtM+1R zjZuiS*jj0l3{g<-C*ZA>+Es8~p`rcXt2-oi2f?**-Mg=$sFCmz6YC@D;nJ6Shz8bY zhc9IBZj8L3BW%B}9bfww0mtq^)=nI!QGV~Hka*wH4k^x4t(4y(Ou=P(x_Vc%qdNPz zKD@-Qpl?a9LyCF4Sg8=dKNR8QABgNBvw*M-C1>=xhXH+!``~zg0Zw!g&t#w5%=Xga z8w3BD)DCU0Zv1JNtL7>LuvZUZGdm+KXnG1+oya=rDqO4VNY&q2d_*_q=wWysWE@xV zfg7&cUA20Cb9MpQ=e3yDjeBS|N3J@6bTne5S<;{32a+_6cU|>xzB^FiBJS`fmeF3w jS;l$TwbkHE+T?|y!HNi5GKw13C)atcv5-Dw3{LzFg?0!P diff --git a/skimage/morphology/_skeletonize.py b/skimage/morphology/_skeletonize.py index 58842c6d..04a65da6 100644 --- a/skimage/morphology/_skeletonize.py +++ b/skimage/morphology/_skeletonize.py @@ -85,10 +85,10 @@ def skeletonize(image): # combinations of 8 binary neighbours. 1's, 2's and 3's are candidates # for removal at each iteration of the algorithm. lut = [ 0,0,0,1,0,0,1,3,0,0,3,1,1,0,1,3,0,0,0,0,0,0,0,0,2,0,2,0,3,0,3,3, - 0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,3,0,2,2, + 0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,3,0,2,2, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 2,0,0,0,0,0,0,0,2,0,0,0,2,0,0,0,3,0,0,0,0,0,0,0,3,0,0,0,3,0,2,0, - 0,1,3,1,0,0,1,3,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 0,0,3,1,0,0,1,3,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 2,3,1,3,0,0,1,3,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 2,3,0,1,0,0,0,1,0,0,0,0,0,0,0,0,3,3,0,1,0,0,0,0,2,2,0,0,2,0,0,0] diff --git a/skimage/morphology/tests/test_skeletonize.py b/skimage/morphology/tests/test_skeletonize.py index 2f9d046e..c9cdbc24 100644 --- a/skimage/morphology/tests/test_skeletonize.py +++ b/skimage/morphology/tests/test_skeletonize.py @@ -92,6 +92,23 @@ class TestSkeletonize(): blocks = correlate(result, mask, mode='constant') assert not numpy.any(blocks == 4) + def test_lut_fix(self): + im = np.zeros((6, 6), np.uint8) + im[1, 2] = 1 + im[2, 2] = 1 + im[2, 3] = 1 + im[3, 3] = 1 + im[3, 4] = 1 + im[4, 4] = 1 + im[4, 5] = 1 + result = skeletonize(im) + expected = np.array([[0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0]], dtype=np.uint8) + assert np.all(result == expected) class TestMedialAxis(): def test_00_00_zeros(self): From 25d94b36c6691866737501008cfc469ac9f12068 Mon Sep 17 00:00:00 2001 From: Pavel Campr Date: Thu, 9 Aug 2012 13:49:26 +0300 Subject: [PATCH 293/648] fix hog.py - orientation and visualization --- skimage/feature/_hog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index 5206034e..98d8f731 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -96,7 +96,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), """ magnitude = sqrt(gx**2 + gy**2) - orientation = arctan2(gy, (gx + 1e-15)) * (180 / pi) + 90 + orientation = arctan2(gy, (gx + 1e-15)) * (180 / pi) % 180 sy, sx = image.shape cx, cy = pixels_per_cell @@ -137,8 +137,8 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), centre = tuple([y * cy + cy // 2, x * cx + cx // 2]) dx = radius * cos(float(o) / orientations * np.pi) dy = radius * sin(float(o) / orientations * np.pi) - rr, cc = draw.bresenham(centre[0] - dx, centre[1] - dy, - centre[0] + dx, centre[1] + dy) + rr, cc = draw.bresenham(centre[0] - dy, centre[1] - dx, + centre[0] + dy, centre[1] + dx) hog_image[rr, cc] += orientation_histogram[y, x, o] """ From 298c1f189092f37d63528c256d7178fccaf7c798 Mon Sep 17 00:00:00 2001 From: pcampr Date: Sat, 18 Aug 2012 15:50:42 +0200 Subject: [PATCH 294/648] fixing multiple bugs in hog.py, adding two tests to test_hog.py --- skimage/feature/_hog.py | 17 +++-- skimage/feature/tests/test_hog.py | 112 ++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 6 deletions(-) diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index 98d8f731..3bd849ab 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -59,7 +59,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), shadowing and illumination variations. """ - if image.ndim > 3: + if image.ndim > 2: raise ValueError("Currently only supports grey-level images") if normalise: @@ -75,6 +75,11 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), e.g. bar like structures in bicycles and limbs in humans. """ + if image.dtype.kind == 'u': + # convert uint image to float + # to avoid problems with subtracting unsigned numbers in np.diff() + image = image.astype('float') + gx = np.zeros(image.shape) gy = np.zeros(image.shape) gx[:, :-1] = np.diff(image, n=1, axis=1) @@ -96,7 +101,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), """ magnitude = sqrt(gx**2 + gy**2) - orientation = arctan2(gy, (gx + 1e-15)) * (180 / pi) % 180 + orientation = arctan2(gy, gx) * (180 / pi) % 180 sy, sx = image.shape cx, cy = pixels_per_cell @@ -113,11 +118,11 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), # isolate orientations in this range temp_ori = np.where(orientation < 180 / orientations * (i + 1), - orientation, 0) + orientation, -1) temp_ori = np.where(orientation >= 180 / orientations * i, - temp_ori, 0) + temp_ori, -1) # select magnitudes for those orientations - cond2 = temp_ori > 0 + cond2 = temp_ori > -1 temp_mag = np.where(cond2, magnitude, 0) temp_filt = uniform_filter(temp_mag, size=(cy, cx)) @@ -176,4 +181,4 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), if visualise: return normalised_blocks.ravel(), hog_image else: - return normalised_blocks.ravel() + return normalised_blocks.ravel() \ No newline at end of file diff --git a/skimage/feature/tests/test_hog.py b/skimage/feature/tests/test_hog.py index 90e08105..ef26d0db 100644 --- a/skimage/feature/tests/test_hog.py +++ b/skimage/feature/tests/test_hog.py @@ -1,6 +1,10 @@ +import numpy as np +from scipy import ndimage from skimage import data from skimage import feature from skimage import img_as_float +from skimage import draw +from numpy.testing import * def test_histogram_of_oriented_gradients(): img = img_as_float(data.lena()[:256, :].mean(axis=2)) @@ -16,6 +20,114 @@ def test_hog_image_size_cell_size_mismatch(): cells_per_block=(1, 1)) assert len(fd) == 9 * (150 // 8) * (200 // 8) +def test_hog_color_image_unsupported_error(): + image = np.zeros((20, 20, 3)) + assert_raises(ValueError, feature.hog, image) + +def test_hog_basic_orientations_and_data_types(): + # scenario: + # 1) create image (with float values) where upper half is filled by zeros, bottom half by 100 + # 2) create unsigned integer version of this image + # 3) calculate feature.hog() for both images, both with 'normalise' option enabled and disabled + # 4) verify that all results are equal where expected + # 5) verify that computed feature vector is as expected + # 6) repeat the scenario for 90, 180 and 270 degrees rotated images + + # size of testing image + width = height = 35 + + image0 = np.zeros((height, width), dtype='float') + image0[height / 2:] = 100 + + for rot in range(4): + # rotate by 0, 90, 180 and 270 degrees + image_float = np.rot90(image0, rot) + + # create uint8 image from image_float + image_uint8 = image_float.astype('uint8') + + (hog_float, hog_img_float) = feature.hog(image_float, orientations=4, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=False) + (hog_uint8, hog_img_uint8) = feature.hog(image_uint8, orientations=4, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=False) + (hog_float_norm, hog_img_float_norm) = feature.hog(image_float, orientations=4, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=True) + (hog_uint8_norm, hog_img_uint8_norm) = feature.hog(image_uint8, orientations=4, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=True) + + # set to True to enable manual debugging with graphical output, + # must be False for automatic testing + if False: + from pylab import * + plt.figure() + plt.subplot(2, 3, 1); plt.imshow(image_float); plt.colorbar(); plt.title('image') + plt.subplot(2, 3, 2); plt.imshow(hog_img_float); plt.colorbar(); plt.title('HOG result visualisation (float img)') + plt.subplot(2, 3, 5); plt.imshow(hog_img_uint8); plt.colorbar(); plt.title('HOG result visualisation (uint8 img)') + plt.subplot(2, 3, 3); plt.imshow(hog_img_float_norm); plt.colorbar(); plt.title('HOG result (normalise) visualisation (float img)') + plt.subplot(2, 3, 6); plt.imshow(hog_img_uint8_norm); plt.colorbar(); plt.title('HOG result (normalise) visualisation (uint8 img)') + plt.show() + + # results (features and visualisation) for float and uint8 images must be almost equal + assert_almost_equal(hog_float, hog_uint8) + assert_almost_equal(hog_img_float, hog_img_uint8) + + # resulting features should be almost equal when 'normalise' is enabled or disabled (for current simple testing image) + assert_almost_equal(hog_float, hog_float_norm, decimal=4) + assert_almost_equal(hog_float, hog_uint8_norm, decimal=4) + + # reshape resulting feature vector to matrix with 4 columns (each corresponding to one of 4 directions), + # only one direction should contain nonzero values (this is manually determined for testing image) + actual = np.max(hog_float.reshape(-1, 4), axis=0) + + if rot in [0, 2]: + # image is rotated by 0 and 180 degrees + desired = [0, 0, 1, 0] + elif rot in [1, 3]: + # image is rotated by 90 and 270 degrees + desired = [1, 0, 0, 0] + + assert_almost_equal(actual, desired, decimal=2) + +def test_hog_orientations_circle(): + # scenario: + # 1) create image with blurred circle in the middle + # 2) calculate feature.hog() + # 3) verify that the resulting feature vector contains uniformly distributed values for all orientations, + # i.e. no orientation is lost or emphasized + # 4) repeat the scenario for other 'orientations' option + + # size of testing image + width = height = 100 + + image = np.zeros((height, width)) + rr, cc = draw.circle(height/2, width/2, width/3) + image[rr, cc] = 100 + image = ndimage.gaussian_filter(image, 2) + + for orientations in range(2, 15): + (hog, hog_img) = feature.hog(image, orientations=orientations, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=False) + + # set to True to enable manual debugging with graphical output, + # must be False for automatic testing + if False: + from pylab import * + + plt.figure() + plt.subplot(1, 2, 1); plt.imshow(image); plt.colorbar(); plt.title('image_float') + plt.subplot(1, 2, 2); plt.imshow(hog_img); plt.colorbar(); plt.title('HOG result visualisation, orientations=%d' % (orientations)) + plt.show() + + # reshape resulting feature vector to matrix with N columns (each column corresponds to one direction), + hog_matrix = hog.reshape(-1, orientations) + + # compute mean values in the resulting feature vector for each direction, + # these values should be almost equal to the global mean value (since the image contains a circle), + # i.e. all directions have same contribution to the result + actual = np.mean(hog_matrix, axis=0) + desired = np.mean(hog_matrix) + assert_almost_equal(actual, desired, decimal=1) + if __name__ == '__main__': from numpy.testing import run_module_suite run_module_suite() From 6761f131adc29800f0b12eeb0859759401112e5e Mon Sep 17 00:00:00 2001 From: pcampr Date: Sat, 18 Aug 2012 16:25:14 +0200 Subject: [PATCH 295/648] fixed imports --- skimage/feature/tests/test_hog.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/skimage/feature/tests/test_hog.py b/skimage/feature/tests/test_hog.py index ef26d0db..8cf45fb8 100644 --- a/skimage/feature/tests/test_hog.py +++ b/skimage/feature/tests/test_hog.py @@ -3,7 +3,7 @@ from scipy import ndimage from skimage import data from skimage import feature from skimage import img_as_float -from skimage import draw +from skimage.draw import draw from numpy.testing import * def test_histogram_of_oriented_gradients(): @@ -32,6 +32,8 @@ def test_hog_basic_orientations_and_data_types(): # 4) verify that all results are equal where expected # 5) verify that computed feature vector is as expected # 6) repeat the scenario for 90, 180 and 270 degrees rotated images + # + # author: Pavel Campr # size of testing image width = height = 35 @@ -58,7 +60,7 @@ def test_hog_basic_orientations_and_data_types(): # set to True to enable manual debugging with graphical output, # must be False for automatic testing if False: - from pylab import * + import matplotlib.pyplot as plt plt.figure() plt.subplot(2, 3, 1); plt.imshow(image_float); plt.colorbar(); plt.title('image') plt.subplot(2, 3, 2); plt.imshow(hog_img_float); plt.colorbar(); plt.title('HOG result visualisation (float img)') @@ -85,6 +87,8 @@ def test_hog_basic_orientations_and_data_types(): elif rot in [1, 3]: # image is rotated by 90 and 270 degrees desired = [1, 0, 0, 0] + else: + raise Exception('Result is not determined for this rotation.') assert_almost_equal(actual, desired, decimal=2) @@ -95,6 +99,8 @@ def test_hog_orientations_circle(): # 3) verify that the resulting feature vector contains uniformly distributed values for all orientations, # i.e. no orientation is lost or emphasized # 4) repeat the scenario for other 'orientations' option + # + # author: Pavel Campr # size of testing image width = height = 100 @@ -111,8 +117,7 @@ def test_hog_orientations_circle(): # set to True to enable manual debugging with graphical output, # must be False for automatic testing if False: - from pylab import * - + import matplotlib.pyplot as plt plt.figure() plt.subplot(1, 2, 1); plt.imshow(image); plt.colorbar(); plt.title('image_float') plt.subplot(1, 2, 2); plt.imshow(hog_img); plt.colorbar(); plt.title('HOG result visualisation, orientations=%d' % (orientations)) From 673d4ec212d78e9a8823bf415e43446ee45ca798 Mon Sep 17 00:00:00 2001 From: pcampr Date: Mon, 20 Aug 2012 22:34:58 +0200 Subject: [PATCH 296/648] several fixes and 3 new tests for Histograms of Oriented Gradients --- CONTRIBUTORS.txt | 3 +++ skimage/feature/_hog.py | 2 +- skimage/feature/tests/test_hog.py | 4 ---- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 5961407d..735bffa9 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -108,3 +108,6 @@ Adaptive thresholding Implementation of Matlab's `regionprops` Estimation of geometric transformation parameters + +- Pavel Campr + Fixes and tests for Histograms of Oriented Gradients. diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index 3bd849ab..ce7db462 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -181,4 +181,4 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), if visualise: return normalised_blocks.ravel(), hog_image else: - return normalised_blocks.ravel() \ No newline at end of file + return normalised_blocks.ravel() diff --git a/skimage/feature/tests/test_hog.py b/skimage/feature/tests/test_hog.py index 8cf45fb8..c9449028 100644 --- a/skimage/feature/tests/test_hog.py +++ b/skimage/feature/tests/test_hog.py @@ -32,8 +32,6 @@ def test_hog_basic_orientations_and_data_types(): # 4) verify that all results are equal where expected # 5) verify that computed feature vector is as expected # 6) repeat the scenario for 90, 180 and 270 degrees rotated images - # - # author: Pavel Campr # size of testing image width = height = 35 @@ -99,8 +97,6 @@ def test_hog_orientations_circle(): # 3) verify that the resulting feature vector contains uniformly distributed values for all orientations, # i.e. no orientation is lost or emphasized # 4) repeat the scenario for other 'orientations' option - # - # author: Pavel Campr # size of testing image width = height = 100 From 6b1dab9f9a7777642b7126b65f2d2ab949d86b5d Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Mon, 20 Aug 2012 22:30:58 +0100 Subject: [PATCH 297/648] MISC move felzenszwalb_cy.pyx to _felzenszwalb_cy.pyx, don't use xrange when not necessary --- doc/examples/plot_segmentations.py | 6 +++--- skimage/segmentation/_felzenszwalb.py | 4 ++-- .../{felzenszwalb_cy.pyx => _felzenszwalb_cy.pyx} | 0 skimage/segmentation/_slic.pyx | 12 ++++++------ skimage/segmentation/setup.py | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) rename skimage/segmentation/{felzenszwalb_cy.pyx => _felzenszwalb_cy.pyx} (100%) diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py index 09012bbd..a8ee7cea 100644 --- a/doc/examples/plot_segmentations.py +++ b/doc/examples/plot_segmentations.py @@ -12,7 +12,7 @@ a basis for more sophisticated algorithms such as CRFs. Felzenszwalb's efficient graph based segmentation ------------------------------------------------- -This fast 2d image segmentation algorithm, proposed in [1]_ is popular in the +This fast 2D image segmentation algorithm, proposed in [1]_ is popular in the computer vision community. The algorithm has a single ``scale`` parameter that influences the segment size. The actual size and number of segments can vary greatly, depending on @@ -25,9 +25,9 @@ local contrast. Quickshift image segmentation ----------------------------- -Quickshift is a relatively recent 2d image segmentation algorithm, based on an +Quickshift is a relatively recent 2D image segmentation algorithm, based on an approximation of kernelized mean-shift. Therefore it belongs to the family of -local mode-seeking algorithms and is applied to the 5d space consisting of +local mode-seeking algorithms and is applied to the 5D space consisting of color information and image location [2]_. One of the benefits of quickshift is that it actually computes a diff --git a/skimage/segmentation/_felzenszwalb.py b/skimage/segmentation/_felzenszwalb.py index 5729bd95..67971a96 100644 --- a/skimage/segmentation/_felzenszwalb.py +++ b/skimage/segmentation/_felzenszwalb.py @@ -1,7 +1,7 @@ import warnings import numpy as np -from .felzenszwalb_cy import _felzenszwalb_grey +from ._felzenszwalb_cy import _felzenszwalb_grey def felzenszwalb(image, scale=1, sigma=0.8, min_size=20): @@ -60,7 +60,7 @@ def felzenszwalb(image, scale=1, sigma=0.8, min_size=20): " wanted?" % image.shape[2]) segmentations = [] # compute quickshift for each channel - for c in xrange(n_channels): + for c in range(n_channels): channel = np.ascontiguousarray(image[:, :, c]) s = _felzenszwalb_grey(channel, scale=scale, sigma=sigma, min_size=min_size) diff --git a/skimage/segmentation/felzenszwalb_cy.pyx b/skimage/segmentation/_felzenszwalb_cy.pyx similarity index 100% rename from skimage/segmentation/felzenszwalb_cy.pyx rename to skimage/segmentation/_felzenszwalb_cy.pyx diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index a4f37fb2..ecb58efe 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -45,7 +45,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, """ image = np.atleast_3d(image) if image.shape[2] != 3: - ValueError("Only 3-channel 2d images are supported.") + ValueError("Only 3-channel 2D images are supported.") image = ndimage.gaussian_filter(img_as_float(image), [sigma, sigma, 0]) if convert2lab: image = rgb2lab(image) @@ -82,21 +82,21 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, cdef np.float_t* current_distance cdef np.float_t* current_pixel cdef double tmp - for i in xrange(max_iter): + for i in range(max_iter): distance.fill(np.inf) changes = 0 current_mean = means.data # assign pixels to means - for k in xrange(n_means): + for k in range(n_means): # compute windows: y_min = int(max(current_mean[0] - 2 * step, 0)) y_max = int(min(current_mean[0] + 2 * step, height)) x_min = int(max(current_mean[1] - 2 * step, 0)) x_max = int(min(current_mean[1] + 2 * step, width)) - for y in xrange(y_min, y_max): + for y in range(y_min, y_max): current_pixel = &image_p[5 * (y * width + x_min)] current_distance = &distance_p[y * width + x_min] - for x in xrange(x_min, x_max): + for x in range(x_min, x_max): mean_entry = current_mean dist_mean = 0 for c in range(5): @@ -117,7 +117,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, break # recompute means: means_list = [np.bincount(nearest_mean.ravel(), - image_yx[:, :, j].ravel()) for j in xrange(5)] + image_yx[:, :, j].ravel()) for j in range(5)] in_mean = np.bincount(nearest_mean.ravel()) in_mean[in_mean == 0] = 1 means = (np.vstack(means_list) / in_mean).T.copy("C") diff --git a/skimage/segmentation/setup.py b/skimage/segmentation/setup.py index ec092ffe..b9e19078 100644 --- a/skimage/segmentation/setup.py +++ b/skimage/segmentation/setup.py @@ -11,8 +11,8 @@ def configuration(parent_package='', top_path=None): config = Configuration('segmentation', parent_package, top_path) - cython(['felzenszwalb_cy.pyx'], working_path=base_path) - config.add_extension('felzenszwalb_cy', sources=['felzenszwalb_cy.c'], + cython(['_felzenszwalb_cy.pyx'], working_path=base_path) + config.add_extension('_felzenszwalb_cy', sources=['_felzenszwalb_cy.c'], include_dirs=[get_numpy_include_dirs()]) cython(['_quickshift.pyx'], working_path=base_path) config.add_extension('_quickshift', sources=['_quickshift.c'], From 1c3aeec2a88de5ebaa7539486fe3930ee0a53dee Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 20 Aug 2012 15:25:40 -0700 Subject: [PATCH 298/648] BUG: Remove uses of xrange for py3 compatibility. --- skimage/morphology/selem.py | 2 +- skimage/segmentation/tests/test_felzenszwalb.py | 2 +- skimage/segmentation/tests/test_quickshift.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index f2234ffb..41be0737 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -84,7 +84,7 @@ def diamond(radius, dtype=np.uint8): are 1 and 0 otherwise. """ half = radius - (I, J) = np.meshgrid(xrange(0, radius * 2 + 1), xrange(0, radius * 2 + 1)) + (I, J) = np.meshgrid(range(0, radius * 2 + 1), range(0, radius * 2 + 1)) s = np.abs(I - half) + np.abs(J - half) return np.array(s <= radius, dtype=dtype) diff --git a/skimage/segmentation/tests/test_felzenszwalb.py b/skimage/segmentation/tests/test_felzenszwalb.py index ebda2a38..8a7abfc4 100644 --- a/skimage/segmentation/tests/test_felzenszwalb.py +++ b/skimage/segmentation/tests/test_felzenszwalb.py @@ -14,7 +14,7 @@ def test_grey(): # we expect 4 segments: assert_equal(len(np.unique(seg)), 4) # that mostly respect the 4 regions: - for i in xrange(4): + for i in range(4): hist = np.histogram(img[seg == i], bins=[0, 0.1, 0.3, 0.5, 1])[0] assert_greater(hist[i], 40) diff --git a/skimage/segmentation/tests/test_quickshift.py b/skimage/segmentation/tests/test_quickshift.py index 5c6eb024..eebcaf0d 100644 --- a/skimage/segmentation/tests/test_quickshift.py +++ b/skimage/segmentation/tests/test_quickshift.py @@ -16,7 +16,7 @@ def test_grey(): # we expect 4 segments: assert_equal(len(np.unique(seg)), 4) # that mostly respect the 4 regions: - for i in xrange(4): + for i in range(4): hist = np.histogram(img[seg == i], bins=[0, 0.1, 0.3, 0.5, 1])[0] assert_greater(hist[i], 20) From d87ed28d8eb1197f0f443d208a9853ff1be9bd94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 21 Aug 2012 08:20:57 +0200 Subject: [PATCH 299/648] Add new package for shared code --- skimage/_shared/__init__.py | 0 skimage/_shared/setup.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 skimage/_shared/__init__.py create mode 100644 skimage/_shared/setup.py diff --git a/skimage/_shared/__init__.py b/skimage/_shared/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/skimage/_shared/setup.py b/skimage/_shared/setup.py new file mode 100644 index 00000000..913a020c --- /dev/null +++ b/skimage/_shared/setup.py @@ -0,0 +1,28 @@ +#!/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('_shared', parent_package, top_path) + config.add_data_dir('tests') + + return config + + +if __name__ == '__main__': + from numpy.distutils.core import setup + setup(maintainer='Scikits-image Developers', + author='Scikits-image Developers', + maintainer_email='scikits-image@googlegroups.com', + description='Transforms', + url='https://github.com/scikits-image/scikits-image', + license='SciPy License (BSD Style)', + **(configuration(top_path='').todict()) + ) From 824997af0a4c0070c5ddaade9c21efaaaac61e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 21 Aug 2012 08:32:20 +0200 Subject: [PATCH 300/648] Move bilinear interpolation code to shared package --- .../interpolation.pxd} | 8 +- skimage/_shared/interpolation.pyx | 104 ++++++++++++++++++ skimage/_shared/setup.py | 5 + skimage/feature/_texture.pyx | 2 +- skimage/setup.py | 1 + skimage/transform/_project.pyx | 102 +---------------- skimage/transform/setup.py | 2 +- 7 files changed, 118 insertions(+), 106 deletions(-) rename skimage/{transform/_project.pxd => _shared/interpolation.pxd} (52%) create mode 100644 skimage/_shared/interpolation.pyx diff --git a/skimage/transform/_project.pxd b/skimage/_shared/interpolation.pxd similarity index 52% rename from skimage/transform/_project.pxd rename to skimage/_shared/interpolation.pxd index dee51026..7ad5d223 100644 --- a/skimage/transform/_project.pxd +++ b/skimage/_shared/interpolation.pxd @@ -1,7 +1,9 @@ -cimport numpy as np -import numpy as np - cdef inline double bilinear_interpolation(double* image, int rows, int cols, double r, double c, char mode, double cval=*) + +cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, + char mode, double cval=*) + +cdef inline int coord_map(int dim, int coord, char mode) diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx new file mode 100644 index 00000000..defa2e9b --- /dev/null +++ b/skimage/_shared/interpolation.pyx @@ -0,0 +1,104 @@ +#cython: cdivison=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +from libc.math cimport ceil, floor + + +cdef inline double bilinear_interpolation(double* image, int rows, int cols, + double r, double c, char mode, + double cval=0): + """Bilinear interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols: int + Shape of image. + r, c : int + Position at which to interpolate. + mode : {'C', 'W', 'M'} + Wrapping mode. Constant, Wrap or Mirror. + cval : double + Constant value to use for constant mode. + + """ + cdef double dr, dc + cdef int 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_pixel(image, rows, cols, minr, minc, mode, cval) \ + + dc * get_pixel(image, rows, cols, minr, maxc, mode, cval) + bottom = (1 - dc) * get_pixel(image, rows, cols, maxr, minc, mode, cval) \ + + dc * get_pixel(image, rows, cols, maxr, maxc, mode, cval) + return (1 - dr) * top + dr * bottom + + +cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, + char mode, double cval=0): + """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', 'M'} + Wrapping mode. Constant, Wrap or Mirror. + cval : double + Constant value to use for constant mode. + + """ + 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 int coord_map(int dim, int 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', 'M'} + Whether to wrap or mirror the coordinate if it + falls outside [0, dim). + + """ + dim = dim - 1 + if mode == 'M': # mirror + 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) + + return coord diff --git a/skimage/_shared/setup.py b/skimage/_shared/setup.py index 913a020c..6e4d1b6b 100644 --- a/skimage/_shared/setup.py +++ b/skimage/_shared/setup.py @@ -13,6 +13,11 @@ def configuration(parent_package='', top_path=None): config = Configuration('_shared', parent_package, top_path) config.add_data_dir('tests') + cython(['interpolation.pyx'], working_path=base_path) + + config.add_extension('interpolation', sources=['interpolation.c'], + include_dirs=[get_numpy_include_dirs()]) + return config diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index ca51e177..20b61513 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -5,7 +5,7 @@ import numpy as np cimport numpy as np from libc.math cimport sin, cos, abs -from skimage.transform._project cimport bilinear_interpolation +from skimage._shared.interpolation cimport bilinear_interpolation def _glcm_loop(np.ndarray[dtype=np.uint8_t, ndim=2, diff --git a/skimage/setup.py b/skimage/setup.py index 02c0b52d..1082ba07 100644 --- a/skimage/setup.py +++ b/skimage/setup.py @@ -6,6 +6,7 @@ def configuration(parent_package='', top_path=None): config = Configuration('skimage', parent_package, top_path) + config.add_subpackage('_shared') config.add_subpackage('color') config.add_subpackage('data') config.add_subpackage('draw') diff --git a/skimage/transform/_project.pyx b/skimage/transform/_project.pyx index 3f31f229..300ba543 100644 --- a/skimage/transform/_project.pyx +++ b/skimage/transform/_project.pyx @@ -5,107 +5,7 @@ cimport numpy as np import numpy as np -from cython.operator import dereference -from libc.math cimport ceil, floor - - -cdef inline double bilinear_interpolation(double* image, int rows, int cols, - double r, double c, char mode, - double cval=0): - """Bilinear interpolation at a given position in the image. - - Parameters - ---------- - image : double array - Input image. - rows, cols: int - Shape of image. - r, c : int - Position at which to interpolate. - mode : {'C', 'W', 'M'} - Wrapping mode. Constant, Wrap or Mirror. - cval : double - Constant value to use for constant mode. - - """ - cdef double dr, dc - cdef int 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_pixel(image, rows, cols, minr, minc, mode, cval) \ - + dc * get_pixel(image, rows, cols, minr, maxc, mode, cval) - bottom = (1 - dc) * get_pixel(image, rows, cols, maxr, minc, mode, cval) \ - + dc * get_pixel(image, rows, cols, maxr, maxc, mode, cval) - return (1 - dr) * top + dr * bottom - - -cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, - char mode, double cval=0): - """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', 'M'} - Wrapping mode. Constant, Wrap or Mirror. - cval : double - Constant value to use for constant mode. - - """ - 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 int coord_map(int dim, int 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', 'M'} - Whether to wrap or mirror the coordinate if it - falls outside [0, dim). - - """ - dim = dim - 1 - if mode == 'M': # mirror - 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) - - return coord +from skimage._shared.interpolation cimport bilinear_interpolation cdef inline _matrix_transform(double x, double y, double* H, double *x_, diff --git a/skimage/transform/setup.py b/skimage/transform/setup.py index 4ecb6ba4..75210b54 100644 --- a/skimage/transform/setup.py +++ b/skimage/transform/setup.py @@ -20,7 +20,7 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('_project', sources=['_project.c'], - include_dirs=[get_numpy_include_dirs()]) + include_dirs=[get_numpy_include_dirs(), '../_shared']) return config From 72473848824f3d837efed4c63ba64dac1513e07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 21 Aug 2012 08:59:29 +0200 Subject: [PATCH 301/648] Add integrate function to shared package --- skimage/_shared/setup.py | 3 +++ skimage/_shared/transform.pxd | 6 +++++ skimage/_shared/transform.pyx | 45 +++++++++++++++++++++++++++++++++ skimage/feature/_template.pyx | 47 ++--------------------------------- skimage/feature/setup.py | 5 ++-- 5 files changed, 58 insertions(+), 48 deletions(-) create mode 100644 skimage/_shared/transform.pxd create mode 100644 skimage/_shared/transform.pyx diff --git a/skimage/_shared/setup.py b/skimage/_shared/setup.py index 6e4d1b6b..ed48916e 100644 --- a/skimage/_shared/setup.py +++ b/skimage/_shared/setup.py @@ -14,9 +14,12 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('tests') cython(['interpolation.pyx'], working_path=base_path) + cython(['transform.pyx'], working_path=base_path) 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()]) return config diff --git a/skimage/_shared/transform.pxd b/skimage/_shared/transform.pxd new file mode 100644 index 00000000..2953ac20 --- /dev/null +++ b/skimage/_shared/transform.pxd @@ -0,0 +1,6 @@ +cimport numpy as cnp +import numpy as np + + +cdef float integrate(cnp.ndarray[float, ndim=2, mode="c"] sat, + int r0, int c0, int r1, int c1) diff --git a/skimage/_shared/transform.pyx b/skimage/_shared/transform.pyx new file mode 100644 index 00000000..b6649852 --- /dev/null +++ b/skimage/_shared/transform.pyx @@ -0,0 +1,45 @@ +#cython: cdivison=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +cimport numpy as cnp +import numpy as np + + +cdef float integrate(cnp.ndarray[float, ndim=2, mode="c"] sat, + int r0, int c0, int r1, int c1): + """ + Using a summed area table / integral image, calculate the sum + over a given window. + + This function is the same as the `integrate` function in + `skimage.transform.integrate`, but this Cython version significantly + speeds up the code. + + Parameters + ---------- + sat : ndarray of float + Summed area table / integral image. + r0, c0 : int + Top-left corner of block to be summed. + r1, c1 : int + Bottom-right corner of block to be summed. + + Returns + ------- + S : int + Sum over the given window. + """ + cdef float S = 0 + + S += sat[r1, c1] + + if (r0 - 1 >= 0) and (c0 - 1 >= 0): + S += sat[r0 - 1, c0 - 1] + + if (r0 - 1 >= 0): + S -= sat[r0 - 1, c1] + + if (c0 - 1 >= 0): + S -= sat[r1, c0 - 1] + return S diff --git a/skimage/feature/_template.pyx b/skimage/feature/_template.pyx index b83761a8..58d48524 100644 --- a/skimage/feature/_template.pyx +++ b/skimage/feature/_template.pyx @@ -35,51 +35,8 @@ cimport numpy as np import numpy as np from scipy.signal import fftconvolve from skimage.transform import integral - - -cdef extern from "math.h": - float sqrt(float x) - float fabs(float x) - - -@cython.boundscheck(False) -cdef float integrate(np.ndarray[float, ndim=2, mode="c"] sat, - int r0, int c0, int r1, int c1): - """ - Using a summed area table / integral image, calculate the sum - over a given window. - - This function is the same as the `integrate` function in - `skimage.transform.integrate`, but this Cython version significantly - speeds up the code. - - Parameters - ---------- - sat : ndarray of float - Summed area table / integral image. - r0, c0 : int - Top-left corner of block to be summed. - r1, c1 : int - Bottom-right corner of block to be summed. - - Returns - ------- - S : int - Sum over the given window. - """ - cdef float S = 0 - - S += sat[r1, c1] - - if (r0 - 1 >= 0) and (c0 - 1 >= 0): - S += sat[r0 - 1, c0 - 1] - - if (r0 - 1 >= 0): - S -= sat[r0 - 1, c1] - - if (c0 - 1 >= 0): - S -= sat[r1, c0 - 1] - return S +from libc.math cimport sqrt, fabs +from skimage._shared.transform cimport integrate @cython.boundscheck(False) diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index 0b0b80bd..9c9074be 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -16,10 +16,9 @@ def configuration(parent_package='', top_path=None): cython(['_template.pyx'], working_path=base_path) config.add_extension('_texture', sources=['_texture.c'], - include_dirs=[get_numpy_include_dirs(), - '../transform']) + include_dirs=[get_numpy_include_dirs(), '../_shared']) config.add_extension('_template', sources=['_template.c'], - include_dirs=[get_numpy_include_dirs()]) + include_dirs=[get_numpy_include_dirs(), '../_shared']) return config From ba171937a134d8c5d4fedd807ca548f114feb942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 21 Aug 2012 09:05:18 +0200 Subject: [PATCH 302/648] Remove unused imports --- skimage/_shared/transform.pxd | 1 - skimage/_shared/transform.pyx | 1 - 2 files changed, 2 deletions(-) diff --git a/skimage/_shared/transform.pxd b/skimage/_shared/transform.pxd index 2953ac20..0edc22a4 100644 --- a/skimage/_shared/transform.pxd +++ b/skimage/_shared/transform.pxd @@ -1,5 +1,4 @@ cimport numpy as cnp -import numpy as np cdef float integrate(cnp.ndarray[float, ndim=2, mode="c"] sat, diff --git a/skimage/_shared/transform.pyx b/skimage/_shared/transform.pyx index b6649852..e4bbfa74 100644 --- a/skimage/_shared/transform.pyx +++ b/skimage/_shared/transform.pyx @@ -3,7 +3,6 @@ #cython: nonecheck=False #cython: wraparound=False cimport numpy as cnp -import numpy as np cdef float integrate(cnp.ndarray[float, ndim=2, mode="c"] sat, From 3b227e226d053685a5cc55b6d6d1f9e48cd826f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 21 Aug 2012 09:20:59 +0200 Subject: [PATCH 303/648] Add nearest neighbour interpolation --- skimage/_shared/interpolation.pxd | 4 ++++ skimage/_shared/interpolation.pyx | 26 +++++++++++++++++++++++++- skimage/transform/_project.pyx | 26 +++++++++++++++++++------- skimage/transform/tests/test_warps.py | 27 +++++++++++++++------------ 4 files changed, 63 insertions(+), 20 deletions(-) diff --git a/skimage/_shared/interpolation.pxd b/skimage/_shared/interpolation.pxd index 7ad5d223..c883d00d 100644 --- a/skimage/_shared/interpolation.pxd +++ b/skimage/_shared/interpolation.pxd @@ -1,4 +1,8 @@ +cdef inline double nearest_neighbour(double* image, int rows, int cols, + double r, double c, char mode, + double cval=*) + cdef inline double bilinear_interpolation(double* image, int rows, int cols, double r, double c, char mode, double cval=*) diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx index defa2e9b..71852ace 100644 --- a/skimage/_shared/interpolation.pyx +++ b/skimage/_shared/interpolation.pyx @@ -2,7 +2,31 @@ #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False -from libc.math cimport ceil, floor +from libc.math cimport ceil, floor, round + + +cdef inline double nearest_neighbour(double* image, int rows, int cols, + double r, double c, char mode, + double cval=0): + """Nearest neighbour interpolation at a given position in the image. + + Parameters + ---------- + image : double array + Input image. + rows, cols: int + Shape of image. + r, c : int + Position at which to interpolate. + mode : {'C', 'W', 'M'} + Wrapping mode. Constant, Wrap or Mirror. + cval : double + Constant value to use for constant mode. + + """ + + return get_pixel(image, rows, cols, round(r), round(c), + mode, cval) cdef inline double bilinear_interpolation(double* image, int rows, int cols, diff --git a/skimage/transform/_project.pyx b/skimage/transform/_project.pyx index 300ba543..4f9a4704 100644 --- a/skimage/transform/_project.pyx +++ b/skimage/transform/_project.pyx @@ -5,7 +5,8 @@ cimport numpy as np import numpy as np -from skimage._shared.interpolation cimport bilinear_interpolation +from skimage._shared.interpolation cimport (nearest_neighbour, + bilinear_interpolation) cdef inline _matrix_transform(double x, double y, double* H, double *x_, @@ -32,7 +33,7 @@ cdef inline _matrix_transform(double x, double y, double* H, double *x_, y_[0] = yy / zz -def homography(np.ndarray image, np.ndarray H, output_shape=None, +def homography(np.ndarray image, np.ndarray H, output_shape=None, int order=1, mode='constant', double cval=0): """ Projective transformation (homography). @@ -67,6 +68,10 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, Transformation matrix H that defines the homography. output_shape : tuple (rows, cols) Shape of the output image generated. + order : {0, 1} + Order of interpolation:: + * 0: Nearest-neighbour interpolation. + * 1: Bilinear interpolation (default). mode : {'constant', 'mirror', 'wrap'} How to handle values outside the image borders. cval : string @@ -104,10 +109,17 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, cdef int rows = img.shape[0] cdef int cols = img.shape[1] - for tfr in range(out_r): - for tfc in range(out_c): - _matrix_transform(tfc, tfr, M.data, &c, &r) - out[tfr, tfc] = bilinear_interpolation(img.data, rows, - cols, r, c, mode_c) + if order == 0: + for tfr in range(out_r): + for tfc in range(out_c): + _matrix_transform(tfc, tfr, M.data, &c, &r) + out[tfr, tfc] = nearest_neighbour(img.data, rows, + cols, r, c, mode_c) + elif order == 1: + for tfr in range(out_r): + for tfc in range(out_c): + _matrix_transform(tfc, tfr, M.data, &c, &r) + out[tfr, tfc] = bilinear_interpolation(img.data, rows, + cols, r, c, mode_c) return out diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index f35b7f57..8c0d81d3 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -48,20 +48,23 @@ def test_fast_homography(): H[:2, :2] = [[C, -S], [S, C]] H[:2, 2] = [tx, ty] - for mode in ('constant', 'mirror', 'wrap'): - p0 = warp(img, ProjectiveTransform(H).inverse, mode=mode, order=1) - p1 = fast_homography(img, H, mode=mode) + tform = ProjectiveTransform(H) - # import matplotlib.pyplot as plt - # f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4) - # ax0.imshow(img) - # ax1.imshow(p0, cmap=plt.cm.gray) - # ax2.imshow(p1, cmap=plt.cm.gray) - # ax3.imshow(np.abs(p0 - p1), cmap=plt.cm.gray) - # plt.show() + for order in range(2): + for mode in ('constant', 'mirror', 'wrap'): + p0 = warp(img, tform.inverse, mode=mode, order=order) + p1 = fast_homography(img, H, mode=mode, order=order) - d = np.mean(np.abs(p0 - p1)) - assert d < 0.001 + # import matplotlib.pyplot as plt + # f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4) + # ax0.imshow(img) + # ax1.imshow(p0, cmap=plt.cm.gray) + # ax2.imshow(p1, cmap=plt.cm.gray) + # ax3.imshow(np.abs(p0 - p1), cmap=plt.cm.gray) + # plt.show() + + d = np.mean(np.abs(p0 - p1)) + assert d < 0.001 def test_swirl(): From a08779e06a72210d061498f0ff2f8201ec1e705b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 21 Aug 2012 15:15:27 +0200 Subject: [PATCH 304/648] Use predefined header files from Cython --- skimage/graph/_spath.pyx | 3 +- skimage/io/_plugins/_colormixer.pyx | 19 +++-------- skimage/segmentation/_quickshift.pyx | 6 +--- skimage/transform/_hough_transform.pyx | 45 +++++++++++--------------- 4 files changed, 25 insertions(+), 48 deletions(-) diff --git a/skimage/graph/_spath.pyx b/skimage/graph/_spath.pyx index f342021d..1624bc7d 100644 --- a/skimage/graph/_spath.pyx +++ b/skimage/graph/_spath.pyx @@ -2,9 +2,8 @@ import _mcp cimport _mcp +from libc.math cimport fabs -cdef extern from "math.h": - double fabs(double f) cdef class MCP_Diff(_mcp.MCP): """MCP_Diff(costs, offsets=None, fully_connected=True) diff --git a/skimage/io/_plugins/_colormixer.pyx b/skimage/io/_plugins/_colormixer.pyx index 5d0dd1b9..ace45c94 100644 --- a/skimage/io/_plugins/_colormixer.pyx +++ b/skimage/io/_plugins/_colormixer.pyx @@ -8,15 +8,10 @@ integers, so currently the only way to clip results efficiently one. """ - +import cython import numpy as np cimport numpy as np - -import cython - -cdef extern from "math.h": - float exp(float) nogil - float pow(float, float) nogil +from libc.math cimport exp, pow @cython.boundscheck(False) @@ -189,7 +184,6 @@ def sigmoid_gamma(np.ndarray[np.uint8_t, ndim=3] img, img[i,j,2] = lut[stateimg[i,j,2]] - @cython.boundscheck(False) def gamma(np.ndarray[np.uint8_t, ndim=3] img, np.ndarray[np.uint8_t, ndim=3] stateimg, @@ -219,7 +213,6 @@ def gamma(np.ndarray[np.uint8_t, ndim=3] img, img[i,j,2] = lut[stateimg[i,j,2]] - @cython.cdivision(True) cdef void rgb_2_hsv(float* RGB, float* HSV) nogil: cdef float R, G, B, H, S, V, MAX, MIN @@ -283,6 +276,7 @@ cdef void rgb_2_hsv(float* RGB, float* HSV) nogil: HSV[1] = S HSV[2] = V + @cython.cdivision(True) cdef void hsv_2_rgb(float* HSV, float* RGB) nogil: cdef float H, S, V @@ -388,6 +382,7 @@ def py_hsv_2_rgb(H, S, V): return (R, G, B) + def py_rgb_2_hsv(R, G, B): '''Convert an HSV value to RGB. @@ -561,9 +556,3 @@ def hsv_multiply(np.ndarray[np.uint8_t, ndim=3] img, img[i, j, 0] = RGB[0] img[i, j, 1] = RGB[1] img[i, j, 2] = RGB[2] - - - - - - diff --git a/skimage/segmentation/_quickshift.pyx b/skimage/segmentation/_quickshift.pyx index 57009ad1..57be1040 100644 --- a/skimage/segmentation/_quickshift.pyx +++ b/skimage/segmentation/_quickshift.pyx @@ -1,6 +1,7 @@ import numpy as np cimport numpy as np cimport cython +from libc.math cimport exp, sqrt from itertools import product from scipy import ndimage @@ -9,11 +10,6 @@ from ..util import img_as_float from ..color import rgb2lab -cdef extern from "math.h": - double exp(double) - double sqrt(double) - - @cython.boundscheck(False) @cython.wraparound(False) @cython.cdivision(True) diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index b34b28e2..906e4464 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -2,27 +2,20 @@ cimport cython import numpy as np cimport numpy as np from random import randint +from libc.math cimport abs, fabs, sqrt, ceil, floor, round +from libc.stdlib cimport rand + + np.import_array() -cdef extern from "stdlib.h": - int rand() - -cdef extern from "math.h": - int abs(int) - double fabs(double) - double sqrt(double) - double ceil(double) - double floor(double) - -cdef double round(double val): - return floor(val + 0.5); cdef double PI_2 = 1.5707963267948966 cdef double NEG_PI_2 = -PI_2 + @cython.boundscheck(False) def _hough(np.ndarray img, np.ndarray[ndim=1, dtype=np.double_t] theta=None): - + if img.ndim != 2: raise ValueError('The input image must be 2D.') @@ -31,7 +24,7 @@ def _hough(np.ndarray img, np.ndarray[ndim=1, dtype=np.double_t] theta=None): cdef np.ndarray[ndim=1, dtype=np.double_t] stheta if theta is None: - theta = np.linspace(PI_2, NEG_PI_2, 180) + theta = np.linspace(PI_2, NEG_PI_2, 180) ctheta = np.cos(theta) stheta = np.sin(theta) @@ -39,14 +32,14 @@ def _hough(np.ndarray img, np.ndarray[ndim=1, dtype=np.double_t] theta=None): # compute the bins and allocate the accumulator array cdef np.ndarray[ndim=2, dtype=np.uint64_t] accum cdef np.ndarray[ndim=1, dtype=np.double_t] bins - cdef int max_distance, offset + cdef int max_distance, offset - max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + + max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + img.shape[1] * img.shape[1]))) accum = np.zeros((max_distance, theta.shape[0]), dtype=np.uint64) bins = np.linspace(-max_distance / 2.0, max_distance / 2.0, max_distance) offset = max_distance / 2 - + # compute the nonzero indexes cdef np.ndarray[ndim=1, dtype=np.npy_intp] x_idxs, y_idxs y_idxs, x_idxs = np.PyArray_Nonzero(img) @@ -58,7 +51,7 @@ def _hough(np.ndarray img, np.ndarray[ndim=1, dtype=np.double_t] theta=None): nthetas = theta.shape[0] for i in range(nidxs): x = x_idxs[i] - y = y_idxs[i] + y = y_idxs[i] for j in range(nthetas): accum_idx = round((ctheta[j] * x + stheta[j] * y)) + offset accum[accum_idx, j] += 1 @@ -94,7 +87,7 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ # maximum line number cutoff cdef int lines_max = 2 ** 15 cdef int xflag, x0, y0, dx0, dy0, dx, dy, gap, x1, y1, good_line, count - max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + + max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + img.shape[1] * img.shape[1]))) accum = np.zeros((max_distance, theta.shape[0]), dtype=np.int64) offset = max_distance / 2 @@ -114,11 +107,11 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ # select random non-zero point count = len(points) if count == 0: - break + break index = rand() % (count) x = points[index][0] y = points[index][1] - del points[index] + del points[index] # if previously eliminated, skip if not mask[y, x]: continue @@ -147,7 +140,7 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ dx0 = 1 else: dx0 = -1 - dy0 = round(b * (1 << shift) / fabs(a)) + dy0 = round(b * (1 << shift) / fabs(a)) y0 = (y0 << shift) + (1 << (shift - 1)) else: if b > 0: @@ -156,7 +149,7 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ dy0 = -1 dx0 = round(a * (1 << shift) / fabs(b)) x0 = (x0 << shift) + (1 << (shift - 1)) - + # pass 1: walk the line, merging lines less than specified gap length for k in range(2): gap = 0 @@ -208,9 +201,9 @@ def _probabilistic_hough(np.ndarray img, int value_threshold, int line_length, \ x1 = px >> shift y1 = py # if non-zero point found, continue the line - if mask[y1, x1]: - if good_line: - accum_idx = round((ctheta[j] * x1 + stheta[j] * y1)) + offset + if mask[y1, x1]: + if good_line: + accum_idx = round((ctheta[j] * x1 + stheta[j] * y1)) + offset accum[accum_idx, max_theta] -= 1 mask[y1, x1] = 0 # exit when the point is the line end From c471d475eb5f62b930bf10ec8e5da08682a33751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 21 Aug 2012 15:43:26 +0200 Subject: [PATCH 305/648] Refactor pnpoly function in Cython and add to shared package --- skimage/_shared/geometry.pxd | 7 ++++ skimage/_shared/geometry.pyx | 27 +++++++++++++ skimage/_shared/setup.py | 2 + skimage/draw/_draw.pyx | 8 +--- skimage/draw/setup.py | 2 +- skimage/morphology/_pnpoly.h | 72 ---------------------------------- skimage/morphology/_pnpoly.pyx | 17 +++----- skimage/morphology/setup.py | 2 +- 8 files changed, 45 insertions(+), 92 deletions(-) create mode 100644 skimage/_shared/geometry.pxd create mode 100644 skimage/_shared/geometry.pyx delete mode 100644 skimage/morphology/_pnpoly.h diff --git a/skimage/_shared/geometry.pxd b/skimage/_shared/geometry.pxd new file mode 100644 index 00000000..b228b23a --- /dev/null +++ b/skimage/_shared/geometry.pxd @@ -0,0 +1,7 @@ + +cdef inline unsigned char point_in_polygon(int nr_verts, double *xp, double *yp, + double x, double y) + +cdef void points_in_polygon(int nr_verts, double *xp, double *yp, + int nr_points, double *x, double *y, + unsigned char *result) diff --git a/skimage/_shared/geometry.pyx b/skimage/_shared/geometry.pyx new file mode 100644 index 00000000..de1ed4f4 --- /dev/null +++ b/skimage/_shared/geometry.pyx @@ -0,0 +1,27 @@ +#cython: cdivison=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + + +cdef inline unsigned char point_in_polygon(int nr_verts, double *xp, double *yp, + double x, double y): + cdef int i + cdef unsigned char c = 0 + cdef int j = nr_verts - 1 + for i in range(nr_verts): + if ( + (((yp[i] <= y) and (y < yp[j])) or + ((yp[j] <= y) and (y < yp[i]))) + and (x < (xp[j] - xp[i]) * (y - yp[i]) / (yp[j] - yp[i]) + xp[i]) + ): + c = not c + j = i + return c + +cdef void points_in_polygon(int nr_verts, double *xp, double *yp, + int nr_points, double *x, double *y, + unsigned char *result): + cdef int n + for n in range(nr_points): + result[n] = point_in_polygon(nr_verts, xp, yp, x[n], y[n]) diff --git a/skimage/_shared/setup.py b/skimage/_shared/setup.py index ed48916e..81113656 100644 --- a/skimage/_shared/setup.py +++ b/skimage/_shared/setup.py @@ -13,9 +13,11 @@ def configuration(parent_package='', top_path=None): config = Configuration('_shared', parent_package, top_path) 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'], diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 9dc1d307..fbd525fa 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -3,11 +3,7 @@ import math from libc.math cimport sqrt cimport numpy as np cimport cython - - -cdef extern from "../morphology/_pnpoly.h": - int pnpoly(int nr_verts, double *xp, double *yp, - double x, double y) +from skimage._shared.geometry cimport point_in_polygon @cython.boundscheck(False) @@ -119,7 +115,7 @@ def polygon(y, x, shape=None): for r in range(minr, maxr+1): for c in range(minc, maxc+1): - if pnpoly(nr_verts, cptr, rptr, c, r): + if point_in_polygon(nr_verts, cptr, rptr, c, r): rr.append(r) cc.append(c) diff --git a/skimage/draw/setup.py b/skimage/draw/setup.py index 4503d664..59067f9a 100644 --- a/skimage/draw/setup.py +++ b/skimage/draw/setup.py @@ -15,7 +15,7 @@ def configuration(parent_package='', top_path=None): cython(['_draw.pyx'], working_path=base_path) config.add_extension('_draw', sources=['_draw.c'], - include_dirs=[get_numpy_include_dirs()]) + include_dirs=[get_numpy_include_dirs(), '../shared']) return config diff --git a/skimage/morphology/_pnpoly.h b/skimage/morphology/_pnpoly.h deleted file mode 100644 index 95c89bcb..00000000 --- a/skimage/morphology/_pnpoly.h +++ /dev/null @@ -1,72 +0,0 @@ -/* `pnpoly` is from - - http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html - - Copyright (c) 1970-2003, Wm. Randolph Franklin - - 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: - - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimers. - 2. Redistributions in binary form must reproduce the above - copyright notice in the documentation and/or other materials - provided with the distribution. - 3. The name of W. Randolph Franklin may not be used to endorse or - promote products derived from this Software without specific - prior written permission. - - 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. */ - -#ifdef __cplusplus -extern "C" { -#endif - -unsigned char pnpoly(int nr_verts, double *xp, double *yp, double x, double y) -{ - int i, j; - unsigned char c = 0; - for (i = 0, j = nr_verts-1; i < nr_verts; j = i++) { - if ((((yp[i]<=y) && (yvx.data, vy.data, m, n) + out[m, n] = point_in_polygon(V, vx.data, vy.data, m, n) return out.view(bool) - + def points_inside_poly(points, verts): """Test whether points lie inside a polygon. @@ -84,8 +77,8 @@ def points_inside_poly(points, verts): cdef np.ndarray[np.uint8_t, ndim=1] out = \ np.zeros(x.shape[0], dtype=np.uint8) - - npnpoly(vx.shape[0], vx.data, vy.data, + + points_in_polygon(vx.shape[0], vx.data, vy.data, x.shape[0], x.data, y.data, out.data) diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index 2dd4a378..bbd176e4 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -28,7 +28,7 @@ def configuration(parent_package='', top_path=None): config.add_extension('_skeletonize_cy', sources=['_skeletonize_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_pnpoly', sources=['_pnpoly.c'], - include_dirs=[get_numpy_include_dirs()]) + include_dirs=[get_numpy_include_dirs(), '../shared']) config.add_extension('_convex_hull', sources=['_convex_hull.c'], include_dirs=[get_numpy_include_dirs()]) From 878554ac35ab5cb8013c7b0b02971a9381a03c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 21 Aug 2012 15:45:18 +0200 Subject: [PATCH 306/648] Fix indentation --- skimage/morphology/_pnpoly.pyx | 52 +++++++++++++++++----------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/skimage/morphology/_pnpoly.pyx b/skimage/morphology/_pnpoly.pyx index 9c897d48..7deb6a50 100644 --- a/skimage/morphology/_pnpoly.pyx +++ b/skimage/morphology/_pnpoly.pyx @@ -48,39 +48,39 @@ def grid_points_inside_poly(shape, verts): def points_inside_poly(points, verts): - """Test whether points lie inside a polygon. + """Test whether points lie inside a polygon. - Parameters - ---------- - points : (N, 2) array - Input points, ``(x, y)``. - verts : (M, 2) array - Vertices of the polygon, sorted either clockwise or anti-clockwise. - The first point may (but does not need to be) duplicated. + Parameters + ---------- + points : (N, 2) array + Input points, ``(x, y)``. + verts : (M, 2) array + Vertices of the polygon, sorted either clockwise or anti-clockwise. + The first point may (but does not need to be) duplicated. - Returns - ------- - mask : (N,) array of bool - True if corresponding point is inside the polygon. + Returns + ------- + mask : (N,) array of bool + True if corresponding point is inside the polygon. - """ - cdef np.ndarray[np.double_t, ndim=1, mode="c"] x, y, vx, vy + """ + cdef np.ndarray[np.double_t, ndim=1, mode="c"] x, y, vx, vy - points = np.asarray(points) - verts = np.asarray(verts) + points = np.asarray(points) + verts = np.asarray(verts) - x = points[:, 0].astype(np.double) - y = points[:, 1].astype(np.double) + x = points[:, 0].astype(np.double) + y = points[:, 1].astype(np.double) - vx = verts[:, 0].astype(np.double) - vy = verts[:, 1].astype(np.double) + vx = verts[:, 0].astype(np.double) + vy = verts[:, 1].astype(np.double) - cdef np.ndarray[np.uint8_t, ndim=1] out = \ - np.zeros(x.shape[0], dtype=np.uint8) + cdef np.ndarray[np.uint8_t, ndim=1] out = \ + np.zeros(x.shape[0], dtype=np.uint8) - points_in_polygon(vx.shape[0], vx.data, vy.data, - x.shape[0], x.data, y.data, - out.data) + points_in_polygon(vx.shape[0], vx.data, vy.data, + x.shape[0], x.data, y.data, + out.data) - return out.astype(bool) + return out.astype(bool) From 20afa7edae4dc0726424140625529e94e881e658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 21 Aug 2012 15:49:18 +0200 Subject: [PATCH 307/648] Add doc string to point in polygon functions --- skimage/_shared/geometry.pyx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/skimage/_shared/geometry.pyx b/skimage/_shared/geometry.pyx index de1ed4f4..e739f988 100644 --- a/skimage/_shared/geometry.pyx +++ b/skimage/_shared/geometry.pyx @@ -6,6 +6,17 @@ cdef inline unsigned char point_in_polygon(int nr_verts, double *xp, double *yp, double x, double y): + """Test whether point lies inside a polygon. + + Parameters + ---------- + nr_verts : int + Number of vertices of polygon. + xp, yp : double array + Coordinates of polygon with length nr_verts. + x, y : double + Coordinates of point. + """ cdef int i cdef unsigned char c = 0 cdef int j = nr_verts - 1 @@ -22,6 +33,21 @@ cdef inline unsigned char point_in_polygon(int nr_verts, double *xp, double *yp, cdef void points_in_polygon(int nr_verts, double *xp, double *yp, int nr_points, double *x, double *y, unsigned char *result): + """Test whether points lie inside a polygon. + + Parameters + ---------- + nr_verts : int + Number of vertices of polygon. + xp, yp : double array + Coordinates of polygon with length nr_verts. + nr_points : int + Number of points to test. + x, y : double array + Coordinates of points. + result : unsigned char array + Test results for each point. + """ cdef int n for n in range(nr_points): result[n] = point_in_polygon(nr_verts, xp, yp, x[n], y[n]) From dc91a0ee2f911c88c190a737726ead6608afec14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 21 Aug 2012 15:50:03 +0200 Subject: [PATCH 308/648] Add empty line between functions --- skimage/_shared/geometry.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/_shared/geometry.pyx b/skimage/_shared/geometry.pyx index e739f988..6f7de4cd 100644 --- a/skimage/_shared/geometry.pyx +++ b/skimage/_shared/geometry.pyx @@ -30,6 +30,7 @@ cdef inline unsigned char point_in_polygon(int nr_verts, double *xp, double *yp, j = i return c + cdef void points_in_polygon(int nr_verts, double *xp, double *yp, int nr_points, double *x, double *y, unsigned char *result): From 38a96b9ac9af7c65b2ac1bf33f28daccaa5c4e23 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Tue, 21 Aug 2012 20:50:52 -0500 Subject: [PATCH 309/648] Remove tag metadata-related fucntions from Image class --- skimage/io/_io.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 20337797..71002395 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -42,20 +42,6 @@ class Image(np.ndarray): setattr(x, tag, kwargs.get(tag, getattr(arr, tag, value))) return x - def __array_finalize__(self, obj): - """Copy object tags.""" - for tag, value in Image.tags.items(): - setattr(self, tag, getattr(obj, tag, value)) - return - - def __reduce__(self): - object_state = list(np.ndarray.__reduce__(self)) - subclass_state = {} - for tag in self.tags: - subclass_state[tag] = getattr(self, tag) - object_state[2] = (object_state[2], subclass_state) - return tuple(object_state) - def _repr_png_(self): return self._repr_image_format('png') @@ -69,19 +55,6 @@ class Image(np.ndarray): str_buffer.close() return return_str - def __setstate__(self, state): - nd_state, subclass_state = state - np.ndarray.__setstate__(self, nd_state) - - for tag in subclass_state: - setattr(self, tag, subclass_state[tag]) - - @property - def exposure(self): - """Return exposure time based on EXIF tag.""" - exposure = self.EXIF['EXIF ExposureTime'].values[0] - return exposure.num / float(exposure.den) - def push(img): """Push an image onto the shared image stack. From de20e482adadaaef2e54e8cbdf7e13cfc7c5dd9f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 22 Aug 2012 22:10:32 -0400 Subject: [PATCH 310/648] ENH: Allow imshow to show images when given file name. --- skimage/io/_io.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 477fe8bc..4df84cd0 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -138,8 +138,8 @@ def imshow(arr, plugin=None, **plugin_args): Parameters ---------- - arr : ndarray - Image data. + arr : ndarray or str + Image data or name of image file. plugin : str Name of plugin to use. By default, the different plugins are tried (starting with the Python Imaging Library) until a suitable @@ -151,6 +151,8 @@ def imshow(arr, plugin=None, **plugin_args): Passed to the given plugin. """ + if isinstance(arr, basestring): + arr = call_plugin('imread', arr, plugin=plugin) return call_plugin('imshow', arr, plugin=plugin, **plugin_args) From ffe2ebacae4292f93f560aca526422e07bbee580 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 22 Aug 2012 22:47:54 -0400 Subject: [PATCH 311/648] BUG: Update filter when edit-box is changed. --- skimage/viewer/widgets/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 625e707b..169a4401 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -160,6 +160,7 @@ class Slider(BaseWidget): self.val = value self._good_editbox_input() + self.callback(self.name, value) def _good_editbox_input(self): self.editbox.setStyleSheet("background-color: rgb(255, 255, 255)") From 9731d4c21bfdd9c3578e6435543b22f528a982c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 23 Aug 2012 17:29:23 +0200 Subject: [PATCH 312/648] Simplify code by removing duplicate loops --- skimage/transform/_project.pyx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/skimage/transform/_project.pyx b/skimage/transform/_project.pyx index 4f9a4704..dbbdfc7d 100644 --- a/skimage/transform/_project.pyx +++ b/skimage/transform/_project.pyx @@ -109,16 +109,13 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, int order=1, cdef int rows = img.shape[0] cdef int cols = img.shape[1] - if order == 0: - for tfr in range(out_r): - for tfc in range(out_c): - _matrix_transform(tfc, tfr, M.data, &c, &r) + for tfr in range(out_r): + for tfc in range(out_c): + _matrix_transform(tfc, tfr, M.data, &c, &r) + if order == 0: out[tfr, tfc] = nearest_neighbour(img.data, rows, cols, r, c, mode_c) - elif order == 1: - for tfr in range(out_r): - for tfc in range(out_c): - _matrix_transform(tfc, tfr, M.data, &c, &r) + elif order == 1: out[tfr, tfc] = bilinear_interpolation(img.data, rows, cols, r, c, mode_c) From 1ac289c5887bca11b4278d855dca70f8fbaa26cc Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Fri, 17 Aug 2012 17:57:43 -0400 Subject: [PATCH 313/648] FIX: corrected docstring for transform.warp --- skimage/transform/_geometric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index c055247e..422737a7 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -721,7 +721,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, mode : string How to handle values outside the image borders. See `scipy.ndimage.map_coordinates` for detail. - cval : string + cval : float Used in conjunction with mode 'constant', the value outside the image boundaries. From 0749f8d9f008fcc32ab533bcc0c87c7cda521d7d Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Fri, 17 Aug 2012 17:59:28 -0400 Subject: [PATCH 314/648] FIX: transform.warp supports cval outside 0-1 range --- skimage/transform/_geometric.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 422737a7..2aecfe48 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -785,4 +785,9 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # The spline filters sometimes return results outside [0, 1], # so clip to ensure valid data - return np.clip(mapped.squeeze(), 0, 1) + clipped = np.clip(mapped, 0, 1) + if mode == 'constant' and not (0 <= cval <= 1): + clipped[mapped == cval] = cval + + # Remove singleton dim introduced by atleast_3d + return clipped.squeeze() From e710fac03e1b8a4fb4bebba3e6dc732546da2722 Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Mon, 20 Aug 2012 19:11:58 -0400 Subject: [PATCH 315/648] Added test case for warp() when cval out of clipping range --- skimage/transform/_geometric.py | 1 + skimage/transform/tests/test_warps.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 2aecfe48..5baaeba1 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -786,6 +786,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # The spline filters sometimes return results outside [0, 1], # so clip to ensure valid data clipped = np.clip(mapped, 0, 1) + if mode == 'constant' and not (0 <= cval <= 1): clipped[mapped == cval] = cval diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index f35b7f57..3ef5a2ee 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -2,7 +2,8 @@ from numpy.testing import assert_array_almost_equal, run_module_suite import numpy as np from skimage.transform import (warp, homography, fast_homography, - SimilarityTransform, ProjectiveTransform) + SimilarityTransform, ProjectiveTransform, + AffineTransform) from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray @@ -74,5 +75,12 @@ def test_swirl(): assert np.mean(np.abs(image - unswirled)) < 0.01 +def test_const_cval_out_of_range(): + img = np.random.randn(100, 100) + warped = warp(img, AffineTransform(translation=(10, 10)), cval=-10) + assert np.any(warped < 0) + + + if __name__ == "__main__": run_module_suite() From c22a176e8d8c368e108cef65144e062abbd87121 Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Mon, 20 Aug 2012 19:13:04 -0400 Subject: [PATCH 316/648] ENH: moved code generating coords out of warp() to _warp_coords --- skimage/transform/_geometric.py | 63 ++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 5baaeba1..9d394372 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -27,6 +27,45 @@ def _stackcopy(a, b): a[:] = b +def _build_coords(orows, ocols, bands, coord_transform_fn, + dtype='float64'): + """ + Return coords object suitable for scipy.ndimage.map_coordinates + + Parameters + ---------- + orows: number of output rows + ocols: number of output columns + bands: number of color bands (aka channels) + coord_transform_fn: something like GeometricTransform.inverse_map + interp_dtype: precision of interpolation e.g. 'float32' or 'float64' + + """ + + coords = np.empty(np.r_[3, (orows, ocols, bands)], dtype=dtype) + + # Reshape grid coordinates into a (P, 2) array of (x, y) pairs + tf_coords = np.indices((ocols, orows), dtype=dtype).reshape(2, -1).T + + # Map each (x, y) pair to the source image according to + # the user-provided mapping + tf_coords = coord_transform_fn(tf_coords) + + # Reshape back to a (2, M, N) coordinate grid + tf_coords = tf_coords.T.reshape((-1, ocols, orows)).swapaxes(1, 2) + + # Place the y-coordinate mapping + _stackcopy(coords[1, ...], tf_coords[0, ...]) + + # Place the x-coordinate mapping + _stackcopy(coords[0, ...], tf_coords[1, ...]) + + # colour-coordinate mapping + coords[2, ...] = range(bands) + + return coords + + class GeometricTransform(object): """Perform geometric transformations on a set of coordinates. @@ -753,30 +792,12 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, if output_shape is None: output_shape = ishape - coords = np.empty(np.r_[3, output_shape], dtype=float) - - ## Construct transformed coordinates - rows, cols = output_shape[:2] - # Reshape grid coordinates into a (P, 2) array of (x, y) pairs - tf_coords = np.indices((cols, rows), dtype=float).reshape(2, -1).T + def coord_transform_fn(*args): + return inverse_map(*args, **map_args) - # Map each (x, y) pair to the source image according to - # the user-provided mapping - tf_coords = inverse_map(tf_coords, **map_args) - - # Reshape back to a (2, M, N) coordinate grid - tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2) - - # Place the y-coordinate mapping - _stackcopy(coords[1, ...], tf_coords[0, ...]) - - # Place the x-coordinate mapping - _stackcopy(coords[0, ...], tf_coords[1, ...]) - - # colour-coordinate mapping - coords[2, ...] = range(bands) + coords = _build_coords(rows, cols, bands, coord_transform_fn) # Prefilter not necessary for order 1 interpolation prefilter = order > 1 From ce200f570f2e7e4b67c24d42a0d0e6f3cdd95243 Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Mon, 20 Aug 2012 19:15:36 -0400 Subject: [PATCH 317/648] ENH: test_warp test case to make sure _warp_coords works for grey and rgb images --- skimage/transform/tests/test_warps.py | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 3ef5a2ee..2c2ecf53 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -1,6 +1,9 @@ +import sys from numpy.testing import assert_array_almost_equal, run_module_suite import numpy as np +import scipy.misc # -- greyscale lena that works without PIL png support + from skimage.transform import (warp, homography, fast_homography, SimilarityTransform, ProjectiveTransform, AffineTransform) @@ -66,6 +69,10 @@ def test_fast_homography(): def test_swirl(): + if not data.checkerboard().shape: + print >> sys.stderr, ('Failed to read image data.checkerboard()' + ' -- Skipping test_fast_homography') + return image = img_as_float(data.checkerboard()) swirl_params = {'radius': 80, 'rotation': 0, 'order': 2, 'mode': 'reflect'} @@ -81,6 +88,28 @@ def test_const_cval_out_of_range(): assert np.any(warped < 0) +def test_warp_identity(): + lena = scipy.misc.lena().astype('float32') / 255 + assert len(lena.shape) == 2 + assert np.allclose(lena, + warp(lena, AffineTransform(rotation=0))) + assert not np.allclose(lena, + warp(lena, AffineTransform(rotation=0.1))) + + rgb_lena = np.transpose( + np.asarray([lena, np.zeros_like(lena), lena]), + (1, 2, 0)) + warped_rgb_lena = warp(rgb_lena, AffineTransform(rotation=0.1)) + + assert np.allclose(rgb_lena, + warp(rgb_lena, AffineTransform(rotation=0))) + assert not np.allclose(rgb_lena, warped_rgb_lena) + # assert no cross-talk between bands + assert np.all(0 == warped_rgb_lena[:, :, 1]) + + + + if __name__ == "__main__": run_module_suite() From e3585ad17a98ec374a4baa453585bcecea3acbee Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Mon, 20 Aug 2012 23:35:21 -0400 Subject: [PATCH 318/648] ENH: renamed and docd _build_coords -> warp_coords --- skimage/transform/_geometric.py | 107 ++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 40 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 9d394372..5dae2c7f 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -27,45 +27,6 @@ def _stackcopy(a, b): a[:] = b -def _build_coords(orows, ocols, bands, coord_transform_fn, - dtype='float64'): - """ - Return coords object suitable for scipy.ndimage.map_coordinates - - Parameters - ---------- - orows: number of output rows - ocols: number of output columns - bands: number of color bands (aka channels) - coord_transform_fn: something like GeometricTransform.inverse_map - interp_dtype: precision of interpolation e.g. 'float32' or 'float64' - - """ - - coords = np.empty(np.r_[3, (orows, ocols, bands)], dtype=dtype) - - # Reshape grid coordinates into a (P, 2) array of (x, y) pairs - tf_coords = np.indices((ocols, orows), dtype=dtype).reshape(2, -1).T - - # Map each (x, y) pair to the source image according to - # the user-provided mapping - tf_coords = coord_transform_fn(tf_coords) - - # Reshape back to a (2, M, N) coordinate grid - tf_coords = tf_coords.T.reshape((-1, ocols, orows)).swapaxes(1, 2) - - # Place the y-coordinate mapping - _stackcopy(coords[1, ...], tf_coords[0, ...]) - - # Place the x-coordinate mapping - _stackcopy(coords[0, ...], tf_coords[1, ...]) - - # colour-coordinate mapping - coords[2, ...] = range(bands) - - return coords - - class GeometricTransform(object): """Perform geometric transformations on a set of coordinates. @@ -737,6 +698,72 @@ def matrix_transform(coords, matrix): return ProjectiveTransform(matrix)(coords) +def warp_coords(orows, ocols, bands, coord_transform_fn, + dtype='float64'): + """ + Return `coords` ndarray suitable for `scipy.ndimage.map_coordinates`, which will yield an + image of shape (orows, ocols, bands) by drawing from source points according to the + `coord_transform_fn`. + + Parameters + ---------- + orows: number of output rows + ocols: number of output columns + bands: number of color bands (aka channels) + coord_transform_fn: something like GeometricTransform.inverse_map + dtype: dtype for return value (should probably be 'float32' or 'float64') + + Notes + ----- + This is a lower-level routine that produces the source coordinates used by `warp()`. + + It is provided separately from `warp` to give additional flexibility to users who would + like, for example, to re-use a particular coordinate mapping, to use specific dtypes at + various points along the the image-warping process, or to implement different + post-processing logic than `warp` performs after the call to `ndimage.map_coordinates`. + + + Examples + -------- + Produce a coordinate map that Shifts an image to the right: + + >>> from skimage import data + >>> from scipy.ndimage import map_coordinates + >>> + >>> def shift_right(xy): + ... xy[:, 0] -= 10 + ... return xy + >>> + >>> coords = warp_coords(30, 30, 3, shift_right) + >>> image = data.camera() + >>> warped_image = map_coordinates(coords, image) + + """ + + coords = np.empty(np.r_[3, (orows, ocols, bands)], dtype=dtype) + + # Reshape grid coordinates into a (P, 2) array of (x, y) pairs + tf_coords = np.indices((ocols, orows), dtype=dtype).reshape(2, -1).T + + # Map each (x, y) pair to the source image according to + # the user-provided mapping + tf_coords = coord_transform_fn(tf_coords) + + # Reshape back to a (2, M, N) coordinate grid + tf_coords = tf_coords.T.reshape((-1, ocols, orows)).swapaxes(1, 2) + + # Place the y-coordinate mapping + _stackcopy(coords[1, ...], tf_coords[0, ...]) + + # Place the x-coordinate mapping + _stackcopy(coords[0, ...], tf_coords[1, ...]) + + # colour-coordinate mapping + coords[2, ...] = range(bands) + + return coords + + def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, mode='constant', cval=0., reverse_map=None): """Warp an image according to a given coordinate transformation. @@ -797,7 +824,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, def coord_transform_fn(*args): return inverse_map(*args, **map_args) - coords = _build_coords(rows, cols, bands, coord_transform_fn) + coords = warp_coords(rows, cols, bands, coord_transform_fn) # Prefilter not necessary for order 1 interpolation prefilter = order > 1 From 96c022535495f6315a28c53e42f973ce1f6da7ef Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Mon, 20 Aug 2012 23:37:03 -0400 Subject: [PATCH 319/648] ENH: test warp with data.lena instead of scipy.misc.lena --- skimage/transform/tests/test_warps.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 2c2ecf53..610c42a8 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -2,8 +2,6 @@ import sys from numpy.testing import assert_array_almost_equal, run_module_suite import numpy as np -import scipy.misc # -- greyscale lena that works without PIL png support - from skimage.transform import (warp, homography, fast_homography, SimilarityTransform, ProjectiveTransform, AffineTransform) @@ -71,7 +69,7 @@ def test_fast_homography(): def test_swirl(): if not data.checkerboard().shape: print >> sys.stderr, ('Failed to read image data.checkerboard()' - ' -- Skipping test_fast_homography') + ' -- Skipping test_swirl') return image = img_as_float(data.checkerboard()) @@ -89,7 +87,12 @@ def test_const_cval_out_of_range(): def test_warp_identity(): - lena = scipy.misc.lena().astype('float32') / 255 + lena = data.lena() + if not lena.shape: + print >> sys.stderr, ('Failed to read image data.lena()' + ' -- Skipping test_warp_identity') + return + lena = img_as_float(rgb2gray(lena)) assert len(lena.shape) == 2 assert np.allclose(lena, warp(lena, AffineTransform(rotation=0))) @@ -108,8 +111,5 @@ def test_warp_identity(): assert np.all(0 == warped_rgb_lena[:, :, 1]) - - - if __name__ == "__main__": run_module_suite() From 5cce39a44ace53223ac547fa50a90d6b9aaf0646 Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Mon, 20 Aug 2012 23:44:31 -0400 Subject: [PATCH 320/648] ENH: remove call in tests to deprecated homography fn --- skimage/transform/tests/test_warps.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 610c42a8..72604b03 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -2,9 +2,10 @@ import sys from numpy.testing import assert_array_almost_equal, run_module_suite import numpy as np -from skimage.transform import (warp, homography, fast_homography, - SimilarityTransform, ProjectiveTransform, - AffineTransform) +from skimage.transform import (warp, fast_homography, + AffineTransform, + ProjectiveTransform, + SimilarityTransform) from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray @@ -31,7 +32,10 @@ def test_homography(): M = np.array([[np.cos(theta),-np.sin(theta),0], [np.sin(theta), np.cos(theta),4], [0, 0, 1]]) - x90 = homography(x, M, order=1) + + x90 = warp(x, + inverse_map=ProjectiveTransform(M).inverse, + order=1) assert_array_almost_equal(x90, np.rot90(x)) From c5dc55cd52e605b7a27be1dd6786cce6f85758ba Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Tue, 21 Aug 2012 16:09:47 -0400 Subject: [PATCH 321/648] ENH: better docstring and test for warp_coords --- skimage/transform/__init__.py | 2 +- skimage/transform/_geometric.py | 29 ++++++++++++++++++--------- skimage/transform/tests/test_warps.py | 18 ++++++++++++++++- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 4721e7d1..b0eee512 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -3,7 +3,7 @@ from .radon_transform import * from .finite_radon_transform import * from ._project import homography as fast_homography from .integral import * -from ._geometric import (warp, estimate_transform, +from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform) from ._warps import swirl, homography diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 5dae2c7f..0e590487 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -699,19 +699,28 @@ def matrix_transform(coords, matrix): def warp_coords(orows, ocols, bands, coord_transform_fn, - dtype='float64'): - """ - Return `coords` ndarray suitable for `scipy.ndimage.map_coordinates`, which will yield an - image of shape (orows, ocols, bands) by drawing from source points according to the - `coord_transform_fn`. + dtype=np.float64): + """Build the source coordinates for the output pixels of an image warp. Parameters ---------- - orows: number of output rows - ocols: number of output columns - bands: number of color bands (aka channels) - coord_transform_fn: something like GeometricTransform.inverse_map - dtype: dtype for return value (should probably be 'float32' or 'float64') + orows : int + number of output rows + ocols : int + number of output columns + bands : int + number of color bands (aka channels) + coord_transform_fn : callable like GeometricTransform.inverse_map + Return input coordinates for given output coordinates + dtype : np.dtype or string + dtype for return value (sane choices: float32 or float64) + + Returns + ------- + coords : (orows * ocols, ) array + Coordinates for `scipy.ndimage.map_coordinates`, that will yield + an image of shape (orows, ocols, bands) by drawing from source + points according to the `coord_transform_fn`. Notes ----- diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 72604b03..28350ff6 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -2,7 +2,7 @@ import sys from numpy.testing import assert_array_almost_equal, run_module_suite import numpy as np -from skimage.transform import (warp, fast_homography, +from skimage.transform import (warp, warp_coords, fast_homography, AffineTransform, ProjectiveTransform, SimilarityTransform) @@ -115,5 +115,21 @@ def test_warp_identity(): assert np.all(0 == warped_rgb_lena[:, :, 1]) +def test_warp_coords_example(): + from skimage import data + from scipy.ndimage import map_coordinates + def shift_right(xy): + print 'xyshape', xy.shape + xy[:, 0] -= 10 + return xy + + image = data.lena().astype(np.float32) + print 'testing' + print image.dtype, image.shape + coords = warp_coords(512, 512, 3, shift_right) + print 'warp_coords', coords.dtype, coords.shape + warped_image = map_coordinates(coords, image) + + if __name__ == "__main__": run_module_suite() From e859520872736a0564a896fd96aab8515fc1debe Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Tue, 21 Aug 2012 16:52:04 -0400 Subject: [PATCH 322/648] FIX: doctest of warp_coords --- skimage/transform/_geometric.py | 9 +++++---- skimage/transform/tests/test_warps.py | 9 +++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 0e590487..2fd2a694 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -717,7 +717,7 @@ def warp_coords(orows, ocols, bands, coord_transform_fn, Returns ------- - coords : (orows * ocols, ) array + coords : (3, orows, ocols, bands) array Coordinates for `scipy.ndimage.map_coordinates`, that will yield an image of shape (orows, ocols, bands) by drawing from source points according to the `coord_transform_fn`. @@ -744,12 +744,12 @@ def warp_coords(orows, ocols, bands, coord_transform_fn, ... return xy >>> >>> coords = warp_coords(30, 30, 3, shift_right) - >>> image = data.camera() - >>> warped_image = map_coordinates(coords, image) + >>> image = data.lena().astype(np.float32) + >>> warped_image = map_coordinates(image, coords) """ - coords = np.empty(np.r_[3, (orows, ocols, bands)], dtype=dtype) + coords = np.empty((3, orows, ocols, bands), dtype=dtype) # Reshape grid coordinates into a (P, 2) array of (x, y) pairs tf_coords = np.indices((ocols, orows), dtype=dtype).reshape(2, -1).T @@ -833,6 +833,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, def coord_transform_fn(*args): return inverse_map(*args, **map_args) + coords = warp_coords(rows, cols, bands, coord_transform_fn) # Prefilter not necessary for order 1 interpolation diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 28350ff6..3cde47ac 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -119,16 +119,13 @@ def test_warp_coords_example(): from skimage import data from scipy.ndimage import map_coordinates def shift_right(xy): - print 'xyshape', xy.shape xy[:, 0] -= 10 return xy image = data.lena().astype(np.float32) - print 'testing' - print image.dtype, image.shape - coords = warp_coords(512, 512, 3, shift_right) - print 'warp_coords', coords.dtype, coords.shape - warped_image = map_coordinates(coords, image) + assert 3 == image.shape[2] + coords = warp_coords(30, 30, 3, shift_right) + warped_image = map_coordinates(image[:, :, 0], coords[:2]) if __name__ == "__main__": From 47c0a506bcdaf2cec28a63bd3aceac970f312124 Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Tue, 21 Aug 2012 16:53:04 -0400 Subject: [PATCH 323/648] ENH: warp_coords docstring --- skimage/transform/_geometric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 2fd2a694..d29baac0 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -717,7 +717,7 @@ def warp_coords(orows, ocols, bands, coord_transform_fn, Returns ------- - coords : (3, orows, ocols, bands) array + coords : (3, orows, ocols, bands) array of dtype `dtype` Coordinates for `scipy.ndimage.map_coordinates`, that will yield an image of shape (orows, ocols, bands) by drawing from source points according to the `coord_transform_fn`. From 3e6a6e37b1b9a4c6ddb6e86ef316b407f9f1de47 Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Tue, 21 Aug 2012 16:55:08 -0400 Subject: [PATCH 324/648] ENH: stricter check on test_const_cval_out_of_range --- skimage/transform/tests/test_warps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 3cde47ac..226a0520 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -87,7 +87,7 @@ def test_swirl(): def test_const_cval_out_of_range(): img = np.random.randn(100, 100) warped = warp(img, AffineTransform(translation=(10, 10)), cval=-10) - assert np.any(warped < 0) + assert np.sum(warped < 0) == (2 * 100 * 10 - 10 * 10) def test_warp_identity(): From 8faf1489556649728be1a58fe2c38eef373e18ae Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Wed, 22 Aug 2012 09:08:14 -0400 Subject: [PATCH 325/648] FIX: wordwrap to 78 --- skimage/transform/_geometric.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index d29baac0..ce2eb7b6 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -724,12 +724,14 @@ def warp_coords(orows, ocols, bands, coord_transform_fn, Notes ----- - This is a lower-level routine that produces the source coordinates used by `warp()`. + This is a lower-level routine that produces the source coordinates used by + `warp()`. - It is provided separately from `warp` to give additional flexibility to users who would - like, for example, to re-use a particular coordinate mapping, to use specific dtypes at - various points along the the image-warping process, or to implement different - post-processing logic than `warp` performs after the call to `ndimage.map_coordinates`. + It is provided separately from `warp` to give additional flexibility to + users who would like, for example, to re-use a particular coordinate + mapping, to use specific dtypes at various points along the the + image-warping process, or to implement different post-processing logic + than `warp` performs after the call to `ndimage.map_coordinates`. Examples From acc86a55084d23d7ef3c0b3b9bdf8f2b2ac38288 Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Wed, 22 Aug 2012 09:11:22 -0400 Subject: [PATCH 326/648] FIX: docstring mentions inverse instead of inverse_map --- skimage/transform/_geometric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index ce2eb7b6..f83f65d1 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -710,7 +710,7 @@ def warp_coords(orows, ocols, bands, coord_transform_fn, number of output columns bands : int number of color bands (aka channels) - coord_transform_fn : callable like GeometricTransform.inverse_map + coord_transform_fn : callable like GeometricTransform.inverse Return input coordinates for given output coordinates dtype : np.dtype or string dtype for return value (sane choices: float32 or float64) From 58bddb1cf2875b5c4a1ae74486ce43b8617706eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 24 Aug 2012 00:05:53 +0200 Subject: [PATCH 327/648] Remove empty line --- skimage/transform/_geometric.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index f83f65d1..cec591a9 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -835,7 +835,6 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, def coord_transform_fn(*args): return inverse_map(*args, **map_args) - coords = warp_coords(rows, cols, bands, coord_transform_fn) # Prefilter not necessary for order 1 interpolation From f750e633c8aad5b499a851b39855cdae7fb02634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 24 Aug 2012 00:14:37 +0200 Subject: [PATCH 328/648] Fix test cases of warps --- skimage/transform/tests/test_warps.py | 50 ++++++++------------------- 1 file changed, 15 insertions(+), 35 deletions(-) diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 226a0520..ac241722 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -1,6 +1,6 @@ -import sys from numpy.testing import assert_array_almost_equal, run_module_suite import numpy as np +from scipy.ndimage import map_coordinates from skimage.transform import (warp, warp_coords, fast_homography, AffineTransform, @@ -28,10 +28,10 @@ def test_homography(): x = np.zeros((5, 5), dtype=np.uint8) x[1, 1] = 255 x = img_as_float(x) - theta = -np.pi/2 - M = np.array([[np.cos(theta),-np.sin(theta),0], - [np.sin(theta), np.cos(theta),4], - [0, 0, 1]]) + theta = -np.pi / 2 + M = np.array([[np.cos(theta), - np.sin(theta), 0], + [np.sin(theta), np.cos(theta), 4], + [0, 0, 1]]) x90 = warp(x, inverse_map=ProjectiveTransform(M).inverse, @@ -40,7 +40,7 @@ def test_homography(): def test_fast_homography(): - img = rgb2gray(data.lena()) + img = rgb2gray(data.lena()).astype(np.uint8) img = img[:, :100] theta = np.deg2rad(30) @@ -71,10 +71,6 @@ def test_fast_homography(): def test_swirl(): - if not data.checkerboard().shape: - print >> sys.stderr, ('Failed to read image data.checkerboard()' - ' -- Skipping test_swirl') - return image = img_as_float(data.checkerboard()) swirl_params = {'radius': 80, 'rotation': 0, 'order': 2, 'mode': 'reflect'} @@ -91,41 +87,25 @@ def test_const_cval_out_of_range(): def test_warp_identity(): - lena = data.lena() - if not lena.shape: - print >> sys.stderr, ('Failed to read image data.lena()' - ' -- Skipping test_warp_identity') - return - lena = img_as_float(rgb2gray(lena)) + lena = img_as_float(rgb2gray(data.lena())) assert len(lena.shape) == 2 - assert np.allclose(lena, - warp(lena, AffineTransform(rotation=0))) - assert not np.allclose(lena, - warp(lena, AffineTransform(rotation=0.1))) - - rgb_lena = np.transpose( - np.asarray([lena, np.zeros_like(lena), lena]), - (1, 2, 0)) + assert np.allclose(lena, warp(lena, AffineTransform(rotation=0))) + assert not np.allclose(lena, warp(lena, AffineTransform(rotation=0.1))) + rgb_lena = np.transpose(np.asarray([lena, np.zeros_like(lena), lena]), + (1, 2, 0)) warped_rgb_lena = warp(rgb_lena, AffineTransform(rotation=0.1)) - - assert np.allclose(rgb_lena, - warp(rgb_lena, AffineTransform(rotation=0))) + assert np.allclose(rgb_lena, warp(rgb_lena, AffineTransform(rotation=0))) assert not np.allclose(rgb_lena, warped_rgb_lena) # assert no cross-talk between bands assert np.all(0 == warped_rgb_lena[:, :, 1]) def test_warp_coords_example(): - from skimage import data - from scipy.ndimage import map_coordinates - def shift_right(xy): - xy[:, 0] -= 10 - return xy - image = data.lena().astype(np.float32) assert 3 == image.shape[2] - coords = warp_coords(30, 30, 3, shift_right) - warped_image = map_coordinates(image[:, :, 0], coords[:2]) + tform = SimilarityTransform(translation=(0, -10)) + coords = warp_coords(30, 30, 3, tform) + warped_image1 = map_coordinates(image[:, :, 0], coords[:2]) if __name__ == "__main__": From 5166e3c8934b820cd92862d2345ad8738ba13f45 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 24 Aug 2012 00:44:34 -0700 Subject: [PATCH 329/648] DOC: Update development guidelines from PR 255 feedback. --- DEVELOPMENT.txt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/DEVELOPMENT.txt b/DEVELOPMENT.txt index e205af85..0dd0a102 100644 --- a/DEVELOPMENT.txt +++ b/DEVELOPMENT.txt @@ -61,10 +61,15 @@ Stylistic Guidelines * When documenting array parameters, use ``image : (M, N) ndarray``, ``image : (M, N, 3) ndarray`` and then refer to ``M`` and ``N`` in the docstring. - * Set up your editor to remove trailing whitespace. + + * Set up your editor to remove trailing whitespace. Follow `PEP08 + `__. Check code with pyflakes / flake8. + * If a function name, say ``segment(...)``, has the same name as the file in which it is implemented, name that file ``_segment.py`` so that it can still - be imported. + be imported. All Cython files start with an underscore, e.g. + ``_some_module.pyx``. + * Functions should support all input image dtypes. Use utility functions such as ``img_as_float`` to help convert to an appropriate type. The output format can be whatever is most efficient. This allows us to string together From 3167a07f1af96d11e63336e6294e30c1629ab99e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 12 Aug 2012 19:37:02 +0200 Subject: [PATCH 330/648] add polygon approximation algorithm --- CONTRIBUTORS.txt | 7 +-- skimage/measure/__init__.py | 1 + skimage/measure/_polygon.py | 88 +++++++++++++++++++++++++++ skimage/measure/tests/test_polygon.py | 27 ++++++++ 4 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 skimage/measure/_polygon.py create mode 100644 skimage/measure/tests/test_polygon.py diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 531ab842..4abe9256 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -105,11 +105,8 @@ Shape views: ``util.shape.view_as_windows`` and ``util.shape.view_as_blocks`` - Johannes Schönberger - Polygon, circle and ellipse drawing functions - Adaptive thresholding - Implementation of Matlab's `regionprops` - Estimation of geometric transformation parameters - Local binary pattern texture classification + Drawing functions, adaptive thresholding, regionprops, geometric + transformations, LBPs, polygon approximations, and more. - Pavel Campr Fixes and tests for Histograms of Oriented Gradients. diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index f5109698..d7397b2f 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -1,3 +1,4 @@ from .find_contours import find_contours from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity +from ._polygon import approximate_polygon \ No newline at end of file diff --git a/skimage/measure/_polygon.py b/skimage/measure/_polygon.py new file mode 100644 index 00000000..0b80599a --- /dev/null +++ b/skimage/measure/_polygon.py @@ -0,0 +1,88 @@ +import numpy as np + + +def approximate_polygon(coords, tolerance): + """Approximate a polygonal chain with the specified tolerance. + + It is based on the Douglas-Peucker algorithm. + + Parameters + ---------- + coords : (N, 2) array + Coordinate array. + tolerance : float + Maximum distance from original points of polygon to approximated + polygonal chain. If tolerance is 0, the original coordinate array + is returned. + + Returns + ------- + coords : (M, 2) array + Approximated polygonal chain where M <= N. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm + """ + if tolerance == 0: + return coords + + chain = np.zeros(coords.shape[0], 'bool') + # pre-allocate distance array for all points + dists = np.zeros(coords.shape[0]) + chain[0] = True + chain[-1] = True + pos_stack = [(0, chain.shape[0] - 1)] + + while 1: + start, end = pos_stack.pop() + # determine properties of current line segment + r0, c0 = coords[start, :] + r1, c1 = coords[end, :] + dr = r1 - r0 + dc = c1 - c0 + segment_angle = - np.arctan2(dr, dc) + segment_dist = c0 * np.sin(segment_angle) + r0 * np.cos(segment_angle) + + # select points in-between line segment + segment_coords = coords[start + 1:end, :] + segment_dists = dists[start + 1:end] + + + # check whether to take perpendicular or euclidean distance with + # inner product of vectors + + # vectors from points -> start and end + dr0 = segment_coords[:, 0] - r0 + dc0 = segment_coords[:, 1] - c0 + dr1 = segment_coords[:, 0] - r1 + dc1 = segment_coords[:, 1] - c1 + # vectors points -> start and end projected on start -> end vector + projected_lengths0 = dr0 * dr + dc0 * dc + projected_lengths1 = - dr1 * dr - dc1 * dc + perp = np.logical_and(projected_lengths0 > 0, + projected_lengths1 > 0) + eucl = np.logical_not(perp) + segment_dists[perp] = np.abs( + segment_coords[perp, 0] * np.cos(segment_angle) + + segment_coords[perp, 1] * np.sin(segment_angle) + - segment_dist + ) + segment_dists[eucl] = np.minimum( + # distance to start point + np.sqrt(dc0[eucl] ** 2 + dr0[eucl] ** 2), + # distance to end point + np.sqrt(dc1[eucl] ** 2 + dr1[eucl] ** 2) + ) + + if np.any(segment_dists > tolerance): + # select point with maximum distance to line + new_end = start + np.argmax(segment_dists) + 1 + pos_stack.append((new_end, end)) + pos_stack.append((start, new_end)) + chain[new_end] = True + + if len(pos_stack) == 0: + break + + return coords[chain, :] diff --git a/skimage/measure/tests/test_polygon.py b/skimage/measure/tests/test_polygon.py new file mode 100644 index 00000000..e539f69c --- /dev/null +++ b/skimage/measure/tests/test_polygon.py @@ -0,0 +1,27 @@ +import numpy as np +from skimage.measure import approximate_polygon + + +def test_approximate_polygon(): + square = np.array([ + [0, 0], [0, 1], [0, 2], [0, 3], + [1, 3], [2, 3], [3, 3], + [3, 2], [3, 1], [3, 0], + [2, 0], [1, 0], [0, 0] + ]) + + out = approximate_polygon(square, 0.1) + np.testing.assert_array_equal(out, square[(0, 3, 6, 9, 12), :]) + + out = approximate_polygon(square, 2.2) + np.testing.assert_array_equal(out, square[(0, 6, 12), :]) + + out = approximate_polygon(square[(0, 1, 3, 4, 5, 6, 7, 9, 11, 12), :], 0.1) + np.testing.assert_array_equal(out, square[(0, 3, 6, 9, 12), :]) + + out = approximate_polygon(square, -1) + np.testing.assert_array_equal(out, square) + + +if __name__ == "__main__": + np.testing.run_module_suite() From ec3ed52da2b0eb6704528d6de79b310370c94b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 12 Aug 2012 21:10:57 +0200 Subject: [PATCH 331/648] add example script for polygon approximation --- doc/examples/plot_approximate_polygon.py | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 doc/examples/plot_approximate_polygon.py diff --git a/doc/examples/plot_approximate_polygon.py b/doc/examples/plot_approximate_polygon.py new file mode 100644 index 00000000..8c1a126d --- /dev/null +++ b/doc/examples/plot_approximate_polygon.py @@ -0,0 +1,35 @@ +""" +==================== +Approximate Polygons +==================== + +This example shows how to approximate polygonal chains with the Douglas-Peucker +algorithm. +""" + +import numpy as np +import matplotlib.pyplot as plt +from skimage.draw import ellipse +from skimage.measure import find_contours, approximate_polygon +from skimage.morphology import erosion + +img = np.zeros((800, 800), 'int32') + +rr, cc = ellipse(250, 250, 180, 230, img.shape) +img[rr, cc] = 1 +rr, cc = ellipse(600, 600, 150, 90, img.shape) +img[rr, cc] = 1 + +plt.gray() +plt.imshow(img) + +strel = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], 'uint8') +for contour in find_contours(img, 0): + coords = approximate_polygon(contour, 4) + plt.plot(coords[:, 1], coords[:, 0], '-r', linewidth=2) + coords = approximate_polygon(contour, 40) + plt.plot(coords[:, 1], coords[:, 0], '-g', linewidth=2) + +plt.axis((0, 800, 0, 800)) +plt.axis('off') +plt.show() From 77e4c37e5c318d39feb190dca1a423908c3911a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 12 Aug 2012 21:12:49 +0200 Subject: [PATCH 332/648] remove unused import and code from polygon approximation example --- doc/examples/plot_approximate_polygon.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/examples/plot_approximate_polygon.py b/doc/examples/plot_approximate_polygon.py index 8c1a126d..cec98492 100644 --- a/doc/examples/plot_approximate_polygon.py +++ b/doc/examples/plot_approximate_polygon.py @@ -11,7 +11,7 @@ import numpy as np import matplotlib.pyplot as plt from skimage.draw import ellipse from skimage.measure import find_contours, approximate_polygon -from skimage.morphology import erosion + img = np.zeros((800, 800), 'int32') @@ -23,7 +23,6 @@ img[rr, cc] = 1 plt.gray() plt.imshow(img) -strel = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], 'uint8') for contour in find_contours(img, 0): coords = approximate_polygon(contour, 4) plt.plot(coords[:, 1], coords[:, 0], '-r', linewidth=2) From 74b358af824232e6419cb40e459aeeaa4805c900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 12 Aug 2012 21:14:27 +0200 Subject: [PATCH 333/648] remove colons in comments --- doc/examples/plot_shapes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 8107fc80..9e586209 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -19,11 +19,11 @@ import numpy as np img = np.zeros((500, 500, 3), 'uint8') -#: draw line +# draw line rr, cc = line(120, 123, 20, 400) img[rr,cc,0] = 255 -#: fill polygon +# fill polygon poly = np.array(( (300, 300), (480, 320), @@ -34,11 +34,11 @@ poly = np.array(( rr, cc = polygon(poly[:,0], poly[:,1], img.shape) img[rr,cc,1] = 255 -#: fill circle +# fill circle rr, cc = circle(200, 200, 100, img.shape) img[rr,cc,:] = (255, 255, 0) -#: fill ellipse +# fill ellipse rr, cc = ellipse(300, 300, 100, 200, img.shape) img[rr,cc,2] = 255 From add94d24cecda037a6b64d3989dde8142731b995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 13 Aug 2012 09:30:28 +0200 Subject: [PATCH 334/648] make loop condition more readable --- skimage/measure/_polygon.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/measure/_polygon.py b/skimage/measure/_polygon.py index 0b80599a..9138faac 100644 --- a/skimage/measure/_polygon.py +++ b/skimage/measure/_polygon.py @@ -33,8 +33,9 @@ def approximate_polygon(coords, tolerance): chain[0] = True chain[-1] = True pos_stack = [(0, chain.shape[0] - 1)] + end_of_chain = False - while 1: + while not end_of_chain: start, end = pos_stack.pop() # determine properties of current line segment r0, c0 = coords[start, :] @@ -83,6 +84,6 @@ def approximate_polygon(coords, tolerance): chain[new_end] = True if len(pos_stack) == 0: - break + end_of_chain = True return coords[chain, :] From dcbea756775f9e531861bd0df4a49480012277b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 13 Aug 2012 22:21:40 +0200 Subject: [PATCH 335/648] improved polygon approximation example script made existing example parameters more readable and add another example with a more complex shape based on a subdivision algorithm. --- doc/examples/plot_approximate_polygon.py | 60 +++++++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/doc/examples/plot_approximate_polygon.py b/doc/examples/plot_approximate_polygon.py index cec98492..ec7e4f4d 100644 --- a/doc/examples/plot_approximate_polygon.py +++ b/doc/examples/plot_approximate_polygon.py @@ -9,6 +9,7 @@ algorithm. import numpy as np import matplotlib.pyplot as plt +from scipy import signal from skimage.draw import ellipse from skimage.measure import find_contours, approximate_polygon @@ -20,15 +21,60 @@ img[rr, cc] = 1 rr, cc = ellipse(600, 600, 150, 90, img.shape) img[rr, cc] = 1 +hand = np.array([[1.64516129, 1.16145833], + [1.64516129, 1.59375 ], + [1.35080645, 1.921875 ], + [1.375 , 2.18229167], + [1.68548387, 1.9375 ], + [1.60887097, 2.55208333], + [1.68548387, 2.69791667], + [1.76209677, 2.56770833], + [1.83064516, 1.97395833], + [1.89516129, 2.75 ], + [1.9516129 , 2.84895833], + [2.01209677, 2.76041667], + [1.99193548, 1.99479167], + [2.11290323, 2.63020833], + [2.2016129 , 2.734375 ], + [2.25403226, 2.60416667], + [2.14919355, 1.953125 ], + [2.30645161, 2.36979167], + [2.39112903, 2.36979167], + [2.41532258, 2.1875 ], + [2.1733871 , 1.703125 ], + [2.07782258, 1.16666667]]) + + +def doo_sabin_subdivision(points): + new_points = np.zeros((2 * (len(points) - 1), 2)) + new_points[::2] = 3 / 4. * points[:-1] + 1 / 4. * points[1:] + new_points[1::2] = 1 / 4. * points[:-1] + 3 / 4. * points[1:] + return new_points + +new_hand = hand.copy() +for _ in range(5): + new_hand = doo_sabin_subdivision(new_hand) + +appr_hand = approximate_polygon(new_hand, tolerance=0.02) + +print "Number of coordinates:", len(hand), len(new_hand), len(appr_hand) + +fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(9, 4)) + +ax1.plot(hand[:, 0], hand[:, 1]) +ax1.plot(new_hand[:, 0], new_hand[:, 1]) +ax1.plot(appr_hand[:, 0], appr_hand[:, 1]) + plt.gray() -plt.imshow(img) +ax2.imshow(img) for contour in find_contours(img, 0): - coords = approximate_polygon(contour, 4) - plt.plot(coords[:, 1], coords[:, 0], '-r', linewidth=2) - coords = approximate_polygon(contour, 40) - plt.plot(coords[:, 1], coords[:, 0], '-g', linewidth=2) + coords = approximate_polygon(contour, tolerance=2.5) + ax2.plot(coords[:, 1], coords[:, 0], '-r', linewidth=2) + coords2 = approximate_polygon(contour, tolerance=39.5) + ax2.plot(coords2[:, 1], coords2[:, 0], '-g', linewidth=2) + print "Number of coordinates:", len(contour), len(coords), len(coords2) + +ax2.axis((0, 800, 0, 800)) -plt.axis((0, 800, 0, 800)) -plt.axis('off') plt.show() From 40c58f33334698d92bcc42e42225ed8c8e6034c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 15 Aug 2012 08:48:40 +0200 Subject: [PATCH 336/648] add subdivision of polygonal curves using B-Splines --- skimage/measure/__init__.py | 2 +- skimage/measure/_polygon.py | 64 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index d7397b2f..1c92d9ee 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -1,4 +1,4 @@ from .find_contours import find_contours from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity -from ._polygon import approximate_polygon \ No newline at end of file +from ._polygon import approximate_polygon, subdivide_polygon \ No newline at end of file diff --git a/skimage/measure/_polygon.py b/skimage/measure/_polygon.py index 9138faac..01bbdd98 100644 --- a/skimage/measure/_polygon.py +++ b/skimage/measure/_polygon.py @@ -1,4 +1,5 @@ import numpy as np +from scipy import signal def approximate_polygon(coords, tolerance): @@ -87,3 +88,66 @@ def approximate_polygon(coords, tolerance): end_of_chain = True return coords[chain, :] + + +SUBDIVISION_DEGREES = { + # degree: (mask_even, mask_odd) + # extracted from (degree + 2)th row of Pascal's triangle + 1: ([1, 1], [1, 1]), + 2: ([3, 1], [1, 3]), + 3: ([1, 6, 1], [0, 4, 4]), + 4: ([5, 10, 1], [1, 10, 5]), + 5: ([1, 15, 15, 1], [0, 6, 20, 6]), + 6: ([7, 35, 21, 1], [1, 21, 35, 7]), + 7: ([1, 28, 70, 28, 1], [0, 8, 56, 56, 8]), +} + + +def subdivide_polygon(coords, degree=2): + """Subdivision of polygonal curves using B-Splines. + + Parameters + ---------- + coords : (N, 2) array + Coordinate array. + method : {1, 2, 3, 4, 5, 6, 7} + Degree of B-Spline. Default is 2. + + Returns + ------- + coords : (M, 2) array + Subdivided coordinate array. + + References + ---------- + .. [1] http://mrl.nyu.edu/publications/subdiv-course2000/coursenotes00.pdf + """ + if degree not in SUBDIVISION_DEGREES: + raise ValueError("Invalid B-Spline degree. Only degree 1 - 7 is " + "supported.") + + circular = np.all(coords[0, :] == coords[-1, :]) + + method = 'valid' + if circular: + coords = coords[:-1, :] + method = 'same' + + mask_even, mask_odd = SUBDIVISION_DEGREES[degree] + mask_even = np.array(mask_even, 'float') / 2 ** degree + mask_odd = np.array(mask_odd, 'float') / 2 ** degree + + even = signal.convolve2d(coords.T, np.atleast_2d(mask_even), mode=method, + boundary='wrap') + odd = signal.convolve2d(coords.T, np.atleast_2d(mask_odd), mode=method, + boundary='wrap') + + out = np.zeros((even.shape[1] + odd.shape[1], 2)) + out[1::2] = even.T + out[::2] = odd.T + + if circular: + # close polygon + out = np.vstack([out, out[0, :]]) + + return out From 339c33eac3e47480d003e7f9b83672fadaeda5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 15 Aug 2012 08:50:31 +0200 Subject: [PATCH 337/648] add note about size if resulting polygonal curves --- skimage/measure/_polygon.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skimage/measure/_polygon.py b/skimage/measure/_polygon.py index 01bbdd98..2d938af7 100644 --- a/skimage/measure/_polygon.py +++ b/skimage/measure/_polygon.py @@ -7,6 +7,9 @@ def approximate_polygon(coords, tolerance): It is based on the Douglas-Peucker algorithm. + Note that the approximated polygon is always within the convex hull of the + original polygon. + Parameters ---------- coords : (N, 2) array @@ -106,6 +109,9 @@ SUBDIVISION_DEGREES = { def subdivide_polygon(coords, degree=2): """Subdivision of polygonal curves using B-Splines. + Note that the resulting curve is always within the convex hull of the + original polygon. + Parameters ---------- coords : (N, 2) array From 76c3574755cf92f59c1cd140caa746d7ec112e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 15 Aug 2012 08:55:43 +0200 Subject: [PATCH 338/648] update polygon example with subdivision --- ...approximate_polygon.py => plot_polygon.py} | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) rename doc/examples/{plot_approximate_polygon.py => plot_polygon.py} (84%) diff --git a/doc/examples/plot_approximate_polygon.py b/doc/examples/plot_polygon.py similarity index 84% rename from doc/examples/plot_approximate_polygon.py rename to doc/examples/plot_polygon.py index ec7e4f4d..1240ff39 100644 --- a/doc/examples/plot_approximate_polygon.py +++ b/doc/examples/plot_polygon.py @@ -9,18 +9,11 @@ algorithm. import numpy as np import matplotlib.pyplot as plt -from scipy import signal from skimage.draw import ellipse -from skimage.measure import find_contours, approximate_polygon +from skimage.measure import find_contours, approximate_polygon, \ + subdivide_polygon -img = np.zeros((800, 800), 'int32') - -rr, cc = ellipse(250, 250, 180, 230, img.shape) -img[rr, cc] = 1 -rr, cc = ellipse(600, 600, 150, 90, img.shape) -img[rr, cc] = 1 - hand = np.array([[1.64516129, 1.16145833], [1.64516129, 1.59375 ], [1.35080645, 1.921875 ], @@ -44,17 +37,12 @@ hand = np.array([[1.64516129, 1.16145833], [2.1733871 , 1.703125 ], [2.07782258, 1.16666667]]) - -def doo_sabin_subdivision(points): - new_points = np.zeros((2 * (len(points) - 1), 2)) - new_points[::2] = 3 / 4. * points[:-1] + 1 / 4. * points[1:] - new_points[1::2] = 1 / 4. * points[:-1] + 3 / 4. * points[1:] - return new_points - +# subdivide polygon using 2nd degree B-Splines new_hand = hand.copy() for _ in range(5): - new_hand = doo_sabin_subdivision(new_hand) + new_hand = subdivide_polygon(new_hand, degree=2) +# approximate subdivided polygon with Douglas-Peucker algorithm appr_hand = approximate_polygon(new_hand, tolerance=0.02) print "Number of coordinates:", len(hand), len(new_hand), len(appr_hand) @@ -65,9 +53,18 @@ ax1.plot(hand[:, 0], hand[:, 1]) ax1.plot(new_hand[:, 0], new_hand[:, 1]) ax1.plot(appr_hand[:, 0], appr_hand[:, 1]) + +# create two ellipses in image +img = np.zeros((800, 800), 'int32') +rr, cc = ellipse(250, 250, 180, 230, img.shape) +img[rr, cc] = 1 +rr, cc = ellipse(600, 600, 150, 90, img.shape) +img[rr, cc] = 1 + plt.gray() ax2.imshow(img) +# approximate / simplify coordinates of the two ellipses for contour in find_contours(img, 0): coords = approximate_polygon(contour, tolerance=2.5) ax2.plot(coords[:, 1], coords[:, 0], '-r', linewidth=2) From f267f656664ea4040cfe78de7c2da90487ef10c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 15 Aug 2012 09:04:06 +0200 Subject: [PATCH 339/648] add test case for subdivision of polygons --- skimage/measure/tests/test_polygon.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/skimage/measure/tests/test_polygon.py b/skimage/measure/tests/test_polygon.py index e539f69c..4644fa84 100644 --- a/skimage/measure/tests/test_polygon.py +++ b/skimage/measure/tests/test_polygon.py @@ -1,15 +1,16 @@ import numpy as np -from skimage.measure import approximate_polygon +from skimage.measure import approximate_polygon, subdivide_polygon + + +square = np.array([ + [0, 0], [0, 1], [0, 2], [0, 3], + [1, 3], [2, 3], [3, 3], + [3, 2], [3, 1], [3, 0], + [2, 0], [1, 0], [0, 0] +]) def test_approximate_polygon(): - square = np.array([ - [0, 0], [0, 1], [0, 2], [0, 3], - [1, 3], [2, 3], [3, 3], - [3, 2], [3, 1], [3, 0], - [2, 0], [1, 0], [0, 0] - ]) - out = approximate_polygon(square, 0.1) np.testing.assert_array_equal(out, square[(0, 3, 6, 9, 12), :]) @@ -23,5 +24,12 @@ def test_approximate_polygon(): np.testing.assert_array_equal(out, square) +def test_subdivide_polygon(): + for degree in range(1, 7): + out = subdivide_polygon(square, degree) + np.testing.assert_array_equal(out[-1], out[0]) + np.testing.assert_equal(out.shape[0], 2 * square.shape[0] - 1) + + if __name__ == "__main__": np.testing.run_module_suite() From 02bf7130172655a4beaf3129fd3c2715eb37d77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 15 Aug 2012 09:08:20 +0200 Subject: [PATCH 340/648] add some more comments to subdivision of polygons code --- skimage/measure/_polygon.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skimage/measure/_polygon.py b/skimage/measure/_polygon.py index 2d938af7..c1eaa9d5 100644 --- a/skimage/measure/_polygon.py +++ b/skimage/measure/_polygon.py @@ -136,10 +136,13 @@ def subdivide_polygon(coords, degree=2): method = 'valid' if circular: + # remove last coordinate because of wrapping coords = coords[:-1, :] + # circular convolution by wrapping boundaries method = 'same' mask_even, mask_odd = SUBDIVISION_DEGREES[degree] + # divide by total weight mask_even = np.array(mask_even, 'float') / 2 ** degree mask_odd = np.array(mask_odd, 'float') / 2 ** degree From 22a89076c26d4539f7b2947afb0fe23eb08c6bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 15 Aug 2012 09:41:19 +0200 Subject: [PATCH 341/648] make subdivision mask code more readable und debuggable --- skimage/measure/_polygon.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/skimage/measure/_polygon.py b/skimage/measure/_polygon.py index c1eaa9d5..06e7a29d 100644 --- a/skimage/measure/_polygon.py +++ b/skimage/measure/_polygon.py @@ -93,7 +93,8 @@ def approximate_polygon(coords, tolerance): return coords[chain, :] -SUBDIVISION_DEGREES = { +# B-Spline subdivision +_SUBDIVISION_MASKS = { # degree: (mask_even, mask_odd) # extracted from (degree + 2)th row of Pascal's triangle 1: ([1, 1], [1, 1]), @@ -128,7 +129,7 @@ def subdivide_polygon(coords, degree=2): ---------- .. [1] http://mrl.nyu.edu/publications/subdiv-course2000/coursenotes00.pdf """ - if degree not in SUBDIVISION_DEGREES: + if degree not in _SUBDIVISION_MASKS: raise ValueError("Invalid B-Spline degree. Only degree 1 - 7 is " "supported.") @@ -141,10 +142,10 @@ def subdivide_polygon(coords, degree=2): # circular convolution by wrapping boundaries method = 'same' - mask_even, mask_odd = SUBDIVISION_DEGREES[degree] + mask_even, mask_odd = _SUBDIVISION_MASKS[degree] # divide by total weight - mask_even = np.array(mask_even, 'float') / 2 ** degree - mask_odd = np.array(mask_odd, 'float') / 2 ** degree + mask_even = np.array(mask_even, np.float) / (2 ** degree) + mask_odd = np.array(mask_odd, np.float) / (2 ** degree) even = signal.convolve2d(coords.T, np.atleast_2d(mask_even), mode=method, boundary='wrap') From 50df20f3b71163f62423c812f8415aef49a426cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 15 Aug 2012 09:41:49 +0200 Subject: [PATCH 342/648] fix wrong parameter name in doc string --- skimage/measure/_polygon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/measure/_polygon.py b/skimage/measure/_polygon.py index 06e7a29d..07af5f58 100644 --- a/skimage/measure/_polygon.py +++ b/skimage/measure/_polygon.py @@ -117,7 +117,7 @@ def subdivide_polygon(coords, degree=2): ---------- coords : (N, 2) array Coordinate array. - method : {1, 2, 3, 4, 5, 6, 7} + degree : {1, 2, 3, 4, 5, 6, 7} Degree of B-Spline. Default is 2. Returns From 26f6bd4fbafe2a73a5c928a78cea18b25ba6bbed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 15 Aug 2012 09:47:34 +0200 Subject: [PATCH 343/648] add test case for non-circular subdivision of polygons --- skimage/measure/tests/test_polygon.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/skimage/measure/tests/test_polygon.py b/skimage/measure/tests/test_polygon.py index 4644fa84..f361bbdc 100644 --- a/skimage/measure/tests/test_polygon.py +++ b/skimage/measure/tests/test_polygon.py @@ -1,6 +1,6 @@ import numpy as np from skimage.measure import approximate_polygon, subdivide_polygon - +from skimage.measure._polygon import _SUBDIVISION_MASKS square = np.array([ [0, 0], [0, 1], [0, 2], [0, 3], @@ -26,9 +26,14 @@ def test_approximate_polygon(): def test_subdivide_polygon(): for degree in range(1, 7): + # test circular out = subdivide_polygon(square, degree) np.testing.assert_array_equal(out[-1], out[0]) np.testing.assert_equal(out.shape[0], 2 * square.shape[0] - 1) + # test non-circular + out = subdivide_polygon(square[:-1], degree) + mask_len = len(_SUBDIVISION_MASKS[degree][0]) + np.testing.assert_equal(out.shape[0], 2 * (square.shape[0] - mask_len)) if __name__ == "__main__": From 8ff263979be76adc3d5e5288404bc1cb2b107eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 16 Aug 2012 23:03:16 +0200 Subject: [PATCH 344/648] Add possibility to preserve ends and update tests --- skimage/measure/_polygon.py | 12 +++++++--- skimage/measure/tests/test_polygon.py | 34 ++++++++++++++++++++------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/skimage/measure/_polygon.py b/skimage/measure/_polygon.py index 07af5f58..633a9418 100644 --- a/skimage/measure/_polygon.py +++ b/skimage/measure/_polygon.py @@ -107,18 +107,21 @@ _SUBDIVISION_MASKS = { } -def subdivide_polygon(coords, degree=2): +def subdivide_polygon(coords, degree=2, preserve_ends=False): """Subdivision of polygonal curves using B-Splines. Note that the resulting curve is always within the convex hull of the - original polygon. + original polygon. Circular polygons stay closed after subdivision. Parameters ---------- coords : (N, 2) array Coordinate array. - degree : {1, 2, 3, 4, 5, 6, 7} + degree : {1, 2, 3, 4, 5, 6, 7}, optional Degree of B-Spline. Default is 2. + preserve_ends : bool, optional + Preserve first and last coordinate of non-circular polygon. Default is + False. Returns ------- @@ -160,4 +163,7 @@ def subdivide_polygon(coords, degree=2): # close polygon out = np.vstack([out, out[0, :]]) + if preserve_ends and not circular: + out = np.vstack([coords[0, :], out, coords[-1, :]]) + return out diff --git a/skimage/measure/tests/test_polygon.py b/skimage/measure/tests/test_polygon.py index f361bbdc..f239c735 100644 --- a/skimage/measure/tests/test_polygon.py +++ b/skimage/measure/tests/test_polygon.py @@ -25,15 +25,31 @@ def test_approximate_polygon(): def test_subdivide_polygon(): - for degree in range(1, 7): - # test circular - out = subdivide_polygon(square, degree) - np.testing.assert_array_equal(out[-1], out[0]) - np.testing.assert_equal(out.shape[0], 2 * square.shape[0] - 1) - # test non-circular - out = subdivide_polygon(square[:-1], degree) - mask_len = len(_SUBDIVISION_MASKS[degree][0]) - np.testing.assert_equal(out.shape[0], 2 * (square.shape[0] - mask_len)) + new_square1 = square + new_square2 = square[:-1] + new_square3 = square[:-1] + # test iterative subdvision + for _ in range(10): + square1, square2, square3 = new_square1, new_square2, new_square3 + # test different B-Spline degrees + for degree in range(1, 7): + mask_len = len(_SUBDIVISION_MASKS[degree][0]) + # test circular + new_square1 = subdivide_polygon(square1, degree) + np.testing.assert_array_equal(new_square1[-1], new_square1[0]) + np.testing.assert_equal(new_square1.shape[0], + 2 * square1.shape[0] - 1) + # test non-circular + new_square2 = subdivide_polygon(square2, degree) + np.testing.assert_equal(new_square2.shape[0], + 2 * (square2.shape[0] - mask_len + 1)) + # test non-circular, preserve_ends + new_square3 = subdivide_polygon(square3, degree, True) + np.testing.assert_equal(new_square3[0], square3[0]) + np.testing.assert_equal(new_square3[-1], square3[-1]) + print mask_len + np.testing.assert_equal(new_square3.shape[0], + 2 * (square3.shape[0] - mask_len + 2)) if __name__ == "__main__": From f9c81e9ed39cad4616a0c809f1424a7e92e51c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 16 Aug 2012 23:06:40 +0200 Subject: [PATCH 345/648] Update subdivision example with preserve_ends parameter --- doc/examples/plot_polygon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_polygon.py b/doc/examples/plot_polygon.py index 1240ff39..6eaef0bf 100644 --- a/doc/examples/plot_polygon.py +++ b/doc/examples/plot_polygon.py @@ -40,7 +40,7 @@ hand = np.array([[1.64516129, 1.16145833], # subdivide polygon using 2nd degree B-Splines new_hand = hand.copy() for _ in range(5): - new_hand = subdivide_polygon(new_hand, degree=2) + new_hand = subdivide_polygon(new_hand, degree=2, preserve_ends=True) # approximate subdivided polygon with Douglas-Peucker algorithm appr_hand = approximate_polygon(new_hand, tolerance=0.02) From eccc41907135cf81b99c4be18a480a9bc705485d Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 24 Aug 2012 03:30:11 -0700 Subject: [PATCH 346/648] BUG: Remove print statement that caused py3 tests to fail. --- skimage/measure/tests/test_polygon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/measure/tests/test_polygon.py b/skimage/measure/tests/test_polygon.py index f239c735..1907d7cb 100644 --- a/skimage/measure/tests/test_polygon.py +++ b/skimage/measure/tests/test_polygon.py @@ -47,7 +47,7 @@ def test_subdivide_polygon(): new_square3 = subdivide_polygon(square3, degree, True) np.testing.assert_equal(new_square3[0], square3[0]) np.testing.assert_equal(new_square3[-1], square3[-1]) - print mask_len + np.testing.assert_equal(new_square3.shape[0], 2 * (square3.shape[0] - mask_len + 2)) From 7ad4b91d6566b8f4951975552f879d911bb7dd85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 24 Aug 2012 12:42:35 +0200 Subject: [PATCH 347/648] Add support for boolean dtype conversion --- skimage/util/dtype.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index 9697e627..35f37889 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -1,7 +1,8 @@ from __future__ import division import numpy as np -__all__ = ['img_as_float', 'img_as_int', 'img_as_uint', 'img_as_ubyte'] +__all__ = ['img_as_float', 'img_as_int', 'img_as_uint', 'img_as_ubyte', + 'img_as_bool'] from .. import get_log log = get_log('dtype_converter') @@ -15,7 +16,8 @@ dtype_range = {np.uint8: (0, 255), integer_types = (np.uint8, np.uint16, np.int8, np.int16) -_supported_types = (np.uint8, np.uint16, np.uint32, +_supported_types = (np.bool_, np.bool8, + np.uint8, np.uint16, np.uint32, np.int8, np.int16, np.int32, np.float32, np.float64) @@ -145,6 +147,10 @@ def convert(image, dtype, force_copy=False, uniform=False): kind_in = dtypeobj_in.kind itemsize = dtypeobj.itemsize itemsize_in = dtypeobj_in.itemsize + + if kind == 'b' or kind_in == 'b': + return dtype(image) + if kind in 'ui': imin = np.iinfo(dtype).min imax = np.iinfo(dtype).max @@ -322,3 +328,26 @@ def img_as_ubyte(image, force_copy=False): """ return convert(image, np.uint8, force_copy) + + +def img_as_bool(image, force_copy=False): + """Convert an image to boolean format. + + Parameters + ---------- + image : ndarray + Input image. + force_copy : bool + Force a copy of the data, irrespective of its current dtype. + + Returns + ------- + out : ndarray of bool (bool_) + Output image. + + Notes + ----- + All non-zero elements are treated as True. + + """ + return convert(image, np.bool_, force_copy) From 4b303f36ccd69b9374f19b2eebd8107059c63674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 24 Aug 2012 12:44:00 +0200 Subject: [PATCH 348/648] Update test cases for boolean dtype conversion --- skimage/util/tests/test_dtype.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/skimage/util/tests/test_dtype.py b/skimage/util/tests/test_dtype.py index 816d5d4c..9803e0af 100644 --- a/skimage/util/tests/test_dtype.py +++ b/skimage/util/tests/test_dtype.py @@ -1,7 +1,7 @@ import numpy as np from numpy.testing import assert_equal, assert_raises from skimage import img_as_int, img_as_float, \ - img_as_uint, img_as_ubyte + img_as_uint, img_as_ubyte, img_as_bool from skimage.util.dtype import convert @@ -86,5 +86,19 @@ def test_copy(): assert y is x assert z is not x + +def test_bool(): + img_ = np.zeros((10, 10), np.bool_) + img8 = np.zeros((10, 10), np.bool8) + img_[1, 1] = True + img8[1, 1] = True + funcs = (img_as_float, img_as_int, img_as_ubyte, img_as_uint, img_as_bool) + for func in funcs: + converted_ = func(img_) + assert np.sum(converted_) == 1 + converted8 = func(img8) + assert np.sum(converted8) == 1 + + if __name__ == '__main__': np.testing.run_module_suite() From 0460479574ba07ee34c6d51d224566dc1bc3ff76 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Fri, 24 Aug 2012 14:24:14 +0200 Subject: [PATCH 349/648] [BUG] fixing import problem --- skimage/feature/_greycomatrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/_greycomatrix.py b/skimage/feature/_greycomatrix.py index 0926117b..45476d33 100644 --- a/skimage/feature/_greycomatrix.py +++ b/skimage/feature/_greycomatrix.py @@ -5,7 +5,7 @@ properties to characterize image textures. import numpy as np -from ._greycomatrix_cy import _glcm_loop +from ._texture import _glcm_loop def greycomatrix(image, distances, angles, levels=256, symmetric=False, From ecd4fb714ab8bc933a79ae7c1a67a6b5aa0dc3db Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 24 Aug 2012 02:49:30 -0700 Subject: [PATCH 350/648] DOC: Add intersphinx so we can refer to numpy, scipy, sklearn docs. --- doc/source/conf.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index c46ab806..0bc673e6 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -26,7 +26,8 @@ sys.path.append(os.path.join(curpath, '..', 'ext')) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.pngmath', 'numpydoc', - 'sphinx.ext.autosummary', 'plot_directive', 'plot2rst'] + 'sphinx.ext.autosummary', 'plot_directive', 'plot2rst', + 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -256,3 +257,11 @@ plot2rst_index_name = 'README' plot2rst_rcparams = {'image.cmap' : 'gray', 'image.interpolation' : 'none'} + +_python_doc_base = 'http://docs.python.org/2.7' +intersphinx_mapping = { + _python_doc_base: None, + 'http://docs.scipy.org/doc/numpy': None, + 'http://docs.scipy.org/doc/scipy/reference': None, + 'http://scikit-learn.org/stable': None +} From 28161eaee60ab696671374d9d20e4b327af17998 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Fri, 24 Aug 2012 15:09:46 +0200 Subject: [PATCH 351/648] ENH: better handling of labels that need to be reordered (this is now done automatically) --- skimage/segmentation/random_walker_segmentation.py | 14 ++++++-------- skimage/segmentation/tests/test_random_walker.py | 5 ++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index eaae60c9..93cba2a7 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -163,7 +163,7 @@ def _build_laplacian(data, mask=None, beta=50): def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, - return_full_prob=False, reorder_labels=False): + return_full_prob=False): """ Random walker algorithm for segmentation from markers. @@ -179,8 +179,8 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, for different phases. Zero-labeled pixels are unlabeled pixels. Negative labels correspond to inactive pixels that are not taken into account (they are removed from the graph). If labels are not - consecutive integers and `reorder_labels` is True, the labels array - will be transformed so that labels are consecutive. + consecutive integers, the labels array will be transformed so that + labels are consecutive. beta : float Penalization coefficient for the random walker motion @@ -220,10 +220,6 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, If True, the probability that a pixel belongs to each of the labels will be returned, instead of only the most likely label. - reorder_labels : bool, default False - If True, labels is transformed so that its values are consecutive - integers. - Returns ------- @@ -308,7 +304,9 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, data = np.atleast_3d(data) if copy: labels = np.copy(labels) - if reorder_labels: + label_values = np.unique(labels) + # Reorder label values to have consecutive integers (no gaps) + if np.any(np.diff(label_values) > 1): mask = labels >= 0 labels[mask] = rank_order(labels[mask])[0].astype(labels.dtype) labels = labels.astype(np.int32) diff --git a/skimage/segmentation/tests/test_random_walker.py b/skimage/segmentation/tests/test_random_walker.py index 6de312e7..4e5a74c3 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -100,9 +100,8 @@ def test_reorder_labels(): lx = 70 ly = 100 data, labels = make_2d_syntheticdata(lx, ly) - labels[labels == 2] == 4 - labels_bf = random_walker(data, labels, beta=90, mode='bf', - reorder_labels=True) + labels[labels == 2] = 4 + labels_bf = random_walker(data, labels, beta=90, mode='bf') assert (labels_bf[25:45, 40:60] == 2).all() return data, labels_bf From e6118a50689583270d86cfabb95b9127b7ebd7c9 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 24 Aug 2012 06:23:35 -0700 Subject: [PATCH 352/648] BUG: Workaround to unit tests failing if PyQt4 not available. --- skimage/viewer/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/__init__.py b/skimage/viewer/__init__.py index e20546b0..638cf43f 100644 --- a/skimage/viewer/__init__.py +++ b/skimage/viewer/__init__.py @@ -1 +1,4 @@ -from viewers import ImageViewer +try: + from viewers import ImageViewer +except ImportError: + print("Could not import PyQt4 -- ImageViewer not available.") From 786821c747c99afc03c9f2ca5f2d858728615720 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 24 Aug 2012 09:55:16 -0700 Subject: [PATCH 353/648] PKG: Update release notes, plot_pr. --- RELEASE.txt | 2 ++ doc/tools/plot_pr.py | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/RELEASE.txt b/RELEASE.txt index d485ae3c..e024d57e 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -7,6 +7,8 @@ How to make a new release of ``skimage`` - Edit ``doc/source/themes/agogo/static/docversions.js`` and commit - Build a clean version of the docs. Run "make" in the root dir, then ``rm build -rf; make html`` in the docs. + - Run ``make html`` again to copy the newly generated ``random.js`` into + place. - Push upstream using "make gh-pages" - Add the version number as a tag in git:: diff --git a/doc/tools/plot_pr.py b/doc/tools/plot_pr.py index 29f3b094..63c0ad44 100644 --- a/doc/tools/plot_pr.py +++ b/doc/tools/plot_pr.py @@ -23,9 +23,12 @@ releases = OrderedDict([ #('0.1', u'2009-10-07 13:52:19 +0200'), #('0.2', u'2009-11-12 14:48:45 +0200'), ('0.3', u'2011-10-10 03:28:47 -0700'), - ('0.4', u'2011-12-03 14:31:32 -0800')]) + ('0.4', u'2011-12-03 14:31:32 -0800'), + ('0.5', u'2012-02-26 21:00:51 -0800'), + ('0.6', u'2012-06-24 21:37:05 -0700')]) -month_duration = 16 + +month_duration = 24 for r in releases: releases[r] = dateutil.parser.parse(releases[r]) From 3e6a51cc2a00baa162c79cd28cd9ab2e0c88fba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 24 Aug 2012 23:17:32 +0200 Subject: [PATCH 354/648] Add bool dtype range --- skimage/util/dtype.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index 35f37889..4a5a6f23 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -7,7 +7,9 @@ __all__ = ['img_as_float', 'img_as_int', 'img_as_uint', 'img_as_ubyte', from .. import get_log log = get_log('dtype_converter') -dtype_range = {np.uint8: (0, 255), +dtype_range = {np.bool_: (False, True), + np.bool8: (False, True), + np.uint8: (0, 255), np.uint16: (0, 65535), np.int8: (-128, 127), np.int16: (-32768, 32767), From 2c84a2135f8d3c566c9b9970544794e52262fb48 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 24 Aug 2012 22:36:20 -0400 Subject: [PATCH 355/648] Remove custom dtype range Bool support was added in gh-#260 --- skimage/viewer/plugins/overlayplugin.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 11ca5ab5..f9d37e59 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -1,15 +1,8 @@ -import numpy as np - -from skimage.util import dtype +from skimage.util.dtype import dtype_range from .base import Plugin from ..utils import ClearColormap -#TODO: Maybe this bool definition should be moved to skimage.util.dtype. -dtype_range = dtype.dtype_range.copy() -dtype_range[np.bool_] = (False, True) - - class OverlayPlugin(Plugin): """Plugin for ImageViewer that displays an overlay on top of main image. From 2c0ff0543c8d2d169262afc80b88ea8abb91096f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 24 Aug 2012 21:28:57 -0400 Subject: [PATCH 356/648] BUG: Closing ImageViewer shouldn't quit parent program --- skimage/viewer/viewers/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 3775e511..1d7d3c3d 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -126,7 +126,7 @@ class ImageViewer(QtGui.QMainWindow): for p in self.plugins: p.show() super(ImageViewer, self).show() - sys.exit(qApp.exec_()) + qApp.exec_() def redraw(self): self.canvas.draw_idle() From ebf4b44a9ebb7c182136f2f091b2896fc281707c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 24 Aug 2012 21:43:47 -0400 Subject: [PATCH 357/648] BUG: Adjust pixel limits based on image data type --- skimage/viewer/viewers/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 3775e511..a5f2e8da 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -5,6 +5,7 @@ import sys from PyQt4 import QtGui, QtCore +from skimage.util.dtype import dtype_range from ..utils import figimage, MatplotlibCanvas @@ -139,6 +140,8 @@ class ImageViewer(QtGui.QMainWindow): def image(self, image): self._img = image self._image_plot.set_array(image) + clim = dtype_range[image.dtype.type] + self._image_plot.set_clim(clim) self.redraw() def reset_image(self): From af708d2e00b8ec41681a593dc33905285100adb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 25 Aug 2012 08:43:42 +0200 Subject: [PATCH 358/648] Fix typo in Cython compiler directives --- skimage/_shared/geometry.pyx | 2 +- skimage/_shared/interpolation.pyx | 2 +- skimage/_shared/transform.pyx | 2 +- skimage/feature/_texture.pyx | 2 +- skimage/transform/_project.pyx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/_shared/geometry.pyx b/skimage/_shared/geometry.pyx index 6f7de4cd..3f4850b0 100644 --- a/skimage/_shared/geometry.pyx +++ b/skimage/_shared/geometry.pyx @@ -1,4 +1,4 @@ -#cython: cdivison=True +#cython: cdivision=True #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx index 71852ace..137004e5 100644 --- a/skimage/_shared/interpolation.pyx +++ b/skimage/_shared/interpolation.pyx @@ -1,4 +1,4 @@ -#cython: cdivison=True +#cython: cdivision=True #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False diff --git a/skimage/_shared/transform.pyx b/skimage/_shared/transform.pyx index e4bbfa74..ba0efc71 100644 --- a/skimage/_shared/transform.pyx +++ b/skimage/_shared/transform.pyx @@ -1,4 +1,4 @@ -#cython: cdivison=True +#cython: cdivision=True #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 20b61513..0e82c306 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -1,4 +1,4 @@ -#cython: cdivison=True +#cython: cdivision=True #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False diff --git a/skimage/transform/_project.pyx b/skimage/transform/_project.pyx index dbbdfc7d..3e9a9295 100644 --- a/skimage/transform/_project.pyx +++ b/skimage/transform/_project.pyx @@ -1,4 +1,4 @@ -#cython: cdivison=True +#cython: cdivision=True #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False From 50d184374ad4260ce2c1cdde26aed5c4a030a03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 25 Aug 2012 13:57:32 +0200 Subject: [PATCH 359/648] Optimize fast homography --- skimage/transform/_project.pyx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skimage/transform/_project.pyx b/skimage/transform/_project.pyx index 3e9a9295..4df6e001 100644 --- a/skimage/transform/_project.pyx +++ b/skimage/transform/_project.pyx @@ -80,13 +80,15 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, int order=1, """ - cdef np.ndarray[dtype=np.double_t, ndim=2] img = image.astype(np.double) + cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] img = \ + np.ascontiguousarray(image, dtype=np.double) cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] M = \ np.ascontiguousarray(np.linalg.inv(H)) if mode not in ('constant', 'wrap', 'mirror'): raise ValueError("Invalid mode specified. Please use " "`constant`, `wrap` or `mirror`.") + cdef char mode_c if mode == 'constant': mode_c = ord('C') elif mode == 'wrap': @@ -94,6 +96,7 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, int order=1, elif mode == 'mirror': mode_c = ord('M') + cdef int out_r, out_c if output_shape is None: out_r = img.shape[0] out_c = img.shape[1] From 945349f963b51e614f737c78825c3eea3d9e6c41 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 25 Aug 2012 08:48:03 -0400 Subject: [PATCH 360/648] DOC: Improve description of reconstruction --- skimage/morphology/greyreconstruct.py | 61 +++++++++++++++++++++------ 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 04ea54a3..9362e3fb 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -14,26 +14,41 @@ import numpy as np from skimage.filter.rank_order import rank_order -def reconstruction(seed, mask, selem=None, offset=None, method='dilation'): +def reconstruction(seed, mask, method='dilation', selem=None, offset=None): """Perform a morphological reconstruction of an image. - Reconstruction requires a "seed" image and a "mask" image of equal shape. - These images set the minimum and maximum possible values of the - reconstructed image. + Morphological reconstruction by dilation is similar to basic morphological + dilation: high-intensity values will replace nearby low-intensity values. + The basic dilation operator, however, uses a structuring element to + determine how far a value in the input image can spread. In contrast, + reconstruction uses two images: a "seed" image, which specifies the values + that spread, and a "mask" image, which gives the maximum allowed value at + each pixel. The mask image, like the structuring element, limits the spread + of high-intensity values. Reconstruction by erosion is simply the inverse: + low-intensity values spread from the seed image and are limited by the mask + image, which represents the minimum allowed value. + + Alternatively, you can think of reconstruction as a way to isolate the + connected regions of an image. For dilation, reconstruction connects + regions marked by local maxima in the seed image: neighboring pixels + less-than-or-equal-to those seeds are connected to the seeded region. + Local maxima with values larger than the seed image will get truncated to + the seed value. Parameters ---------- seed : ndarray - The seed image; a.k.a. marker image. + The seed image (a.k.a. marker image), which specifies the values that + are dilated or eroded. mask : ndarray - The maximum allowed value at each point. + The maximum (dilation) / minimum (erosion) allowed value at each pixel. + method : {'dilation'|'erosion'} + Perform reconstruction by dilation or erosion. In dilation (or + erosion), the seed image is dilated (or eroded) until limited by the + mask image. For dilation, each seed value must be less than or equal + to the corresponding mask value; for erosion, the reverse is true. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - method : {'dilation'|'erosion'} - Perform reconstruction by dilation or erosion. In dilation (erosion), - the seed image is dilated (eroded) until limited by the mask image. - For dilation, each seed value must be less than or equal to the - corresponding mask value; for erosion, the reverse is true. Returns ------- @@ -42,11 +57,29 @@ def reconstruction(seed, mask, selem=None, offset=None, method='dilation'): Examples -------- - Here, we try to extract the bright features of an image by subtracting a - background image created by reconstruction. - >>> import numpy as np >>> from skimage.morphology import reconstruction + + First, we create a sinusoidal mask image w/ peaks at middle and ends. + >>> x = np.linspace(0, 4 * np.pi) + >>> y_mask = np.cos(x) + + Then, we create a seed image initialized to the minimum mask value (for + reconstruction by dilation, min-intensity values don't spread) and add + "seeds" to the left and right peak, but at a fraction of peak value (1). + >>> y_seed = y_mask.min() * np.ones_like(x) + >>> y_seed[0] = 0.5 + >>> y_seed[-1] = 0 + >>> y_rec = reconstruction(y_seed, y_mask) + + The reconstructed image (or curve, in this case) is exactly the same as the + mask image, except that the peaks are truncated to 0.5 and 0. The middle + peak disappears completely: Since there were no seed values in this peak + region, its reconstructed value is truncated to the surrounding value (-1). + + As a more practical example, we try to extract the bright features of an + image by subtracting a background image created by reconstruction. + >>> y, x = np.mgrid[:20:0.5, :20:0.5] >>> bumps = np.sin(x) + np.sin(y) From 666d6f4f095adedb7f64225a609b2ab2bff11b66 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 25 Aug 2012 08:58:54 -0400 Subject: [PATCH 361/648] DOC: Bump up Cython version up to 0.16 This PR uses typed memoryviews, which were introduced in 0.16. --- DEPENDS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index 2792870b..f06ceca3 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -2,7 +2,7 @@ Build Requirements ------------------ * `Python >= 2.5 `__ * `Numpy >= 1.6 `__ -* `Cython >= 0.15 `__ +* `Cython >= 0.16 `__ `Matplotlib >= 1.0 `__ is needed to generate the From b14514e0181975064ea23219d60dca790cf15428 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 25 Aug 2012 12:06:03 -0400 Subject: [PATCH 362/648] BUG: Fix nosetest and autodoc errors when PyQt4 not available nose and autodoc imports the viewer modules so all PyQt4 imports must be wrapped in a try-except block. In addition, any classes derived from PyQt4 must be proxied since the class definition are run on import. This is really hacky. --- skimage/viewer/plugins/base.py | 17 +++++++++++++---- skimage/viewer/utils/core.py | 18 ++++++++++++++---- skimage/viewer/viewers/core.py | 9 +++++++-- skimage/viewer/widgets/core.py | 13 +++++++++---- skimage/viewer/widgets/history.py | 5 ++++- 5 files changed, 47 insertions(+), 15 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index fc2951b2..ae240c20 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,14 +1,23 @@ """ Base class for Plugins that interact with ImageViewer. """ -from PyQt4 import QtGui -from PyQt4.QtCore import Qt -import matplotlib as mpl +try: + from PyQt4 import QtGui + from PyQt4.QtCore import Qt + from PyQt4.QtGui import QDialog +except ImportError: + QDialog = object # hack to prevent nosetest and autodoc errors + print("Could not import PyQt4 -- skimage.viewer not available.") + +try: + import matplotlib as mpl +except ImportError: + print("Could not import matplotlib -- skimage.viewer not available.") from ..utils import RequiredAttr -class Plugin(QtGui.QDialog): +class Plugin(QDialog): """Base class for plugins that interact with an ImageViewer. A plugin connects an image filter (or another function) to an image viewer. diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 36476df8..c311a1a1 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -1,8 +1,18 @@ import numpy as np -import matplotlib.pyplot as plt -from matplotlib.colors import LinearSegmentedColormap -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg -from PyQt4 import QtGui + +try: + import matplotlib.pyplot as plt + from matplotlib.colors import LinearSegmentedColormap + from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg +except ImportError: + FigureCanvasQTAgg = object # hack to prevent nosetest and autodoc errors + LinearSegmentedColormap = object + print("Could not import matplotlib -- skimage.viewer not available.") + +try: + from PyQt4 import QtGui +except ImportError: + print("Could not import PyQt4 -- skimage.viewer not available.") __all__ = ['figimage', 'LinearColormap', 'ClearColormap', 'MatplotlibCanvas', diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 1d7d3c3d..b3d0d260 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -3,7 +3,12 @@ ImageViewer class for viewing and interacting with images. """ import sys -from PyQt4 import QtGui, QtCore +try: + from PyQt4 import QtGui, QtCore + from PyQt4.QtGui import QMainWindow +except ImportError: + QMainWindow = object # hack to prevent nosetest and autodoc errors + print("Could not import PyQt4 -- skimage.viewer not available.") from ..utils import figimage, MatplotlibCanvas @@ -18,7 +23,7 @@ class ImageCanvas(MatplotlibCanvas): super(ImageCanvas, self).__init__(parent, self.fig, **kwargs) -class ImageViewer(QtGui.QMainWindow): +class ImageViewer(QMainWindow): """Viewer for displaying images. This viewer is a simple container object that holds a Matplotlib axes diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 169a4401..0ae2d1e8 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -15,9 +15,14 @@ parameter type specified by its `ptype` attribute, which can be: property of the same name that updates the display. """ -from PyQt4.QtCore import Qt -from PyQt4 import QtGui -from PyQt4 import QtCore +try: + from PyQt4.QtCore import Qt + from PyQt4 import QtGui + from PyQt4 import QtCore + from PyQt4.QtGui import QWidget +except ImportError: + QWidget = object # hack to prevent nosetest and autodoc errors + print("Could not import PyQt4 -- skimage.viewer not available.") from ..utils import RequiredAttr @@ -25,7 +30,7 @@ from ..utils import RequiredAttr __all__ = ['BaseWidget', 'Slider', 'ComboBox'] -class BaseWidget(QtGui.QWidget): +class BaseWidget(QWidget): plugin = RequiredAttr("Widget is not attached to a Plugin.") diff --git a/skimage/viewer/widgets/history.py b/skimage/viewer/widgets/history.py index bb65b7dd..efc8a21c 100644 --- a/skimage/viewer/widgets/history.py +++ b/skimage/viewer/widgets/history.py @@ -1,7 +1,10 @@ import os from textwrap import dedent -from PyQt4 import QtGui +try: + from PyQt4 import QtGui +except ImportError: + print("Could not import PyQt4 -- skimage.viewer not available.") from skimage import io from .core import BaseWidget From be339417019a8cb742de092a66248e4ade4a16af Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 25 Aug 2012 12:11:46 -0400 Subject: [PATCH 363/648] Change error to warning so that autodoc doesn't fail. --- skimage/viewer/utils/core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index c311a1a1..5c5f878c 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -1,3 +1,5 @@ +import warnings + import numpy as np try: @@ -28,7 +30,7 @@ class RequiredAttr(object): def __get__(self, obj, objtype): if self.val is None: - raise RuntimeError(self.msg) + warnings.warn(self.msg) return self.val def __set__(self, obj, val): From e350db23a2c91c008f2193193fd43d5a4a2cfc75 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 25 Aug 2012 14:04:48 -0400 Subject: [PATCH 364/648] Change reconstruction to support Cython 0.15. This removes use of Cython's typed memoryviews. This reverts commit b5d91069664b4f323df9b2a201966a4e8ac8160d: "ENH: Use Cython data types instead of Numpy dtypes." Conflicts: skimage/morphology/_greyreconstruct.pyx --- DEPENDS.txt | 2 +- skimage/morphology/_greyreconstruct.pyx | 29 ++++++++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index 7793abdb..b2858256 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -2,7 +2,7 @@ Build Requirements ------------------ * `Python >= 2.5 `__ * `Numpy >= 1.6 `__ -* `Cython >= 0.16 `__ +* `Cython >= 0.15 `__ `Matplotlib >= 1.0 `__ is needed to generate the examples in the documentation. diff --git a/skimage/morphology/_greyreconstruct.pyx b/skimage/morphology/_greyreconstruct.pyx index 5d2cb0c5..e8a84f3b 100644 --- a/skimage/morphology/_greyreconstruct.pyx +++ b/skimage/morphology/_greyreconstruct.pyx @@ -8,12 +8,21 @@ All rights reserved. Original author: Lee Kamentsky """ +cimport numpy as cnp cimport cython @cython.boundscheck(False) -def reconstruction_loop(unsigned int[:] ranks, int[:] prev, int[:] next, - int[:] strides, int current_idx, int image_stride): +def reconstruction_loop(cnp.ndarray[dtype=cnp.uint32_t, ndim=1, + negative_indices=False, mode='c'] aranks, + cnp.ndarray[dtype=cnp.int32_t, ndim=1, + negative_indices=False, mode='c'] aprev, + cnp.ndarray[dtype=cnp.int32_t, ndim=1, + negative_indices=False, mode='c'] anext, + cnp.ndarray[dtype=cnp.int32_t, ndim=1, + negative_indices=False, mode='c'] astrides, + int current_idx, + int image_stride): """The inner loop for reconstruction. This algorithm uses the rank-order of pixels. If low intensity pixels have @@ -28,20 +37,24 @@ def reconstruction_loop(unsigned int[:] ranks, int[:] prev, int[:] next, Parameters ---------- - ranks : array + aranks : array The rank order of the flattened seed and mask images. - prev, next: arrays + aprev, anext: arrays Indices of previous and next pixels in rank sorted order. - strides : array + astrides : array Strides to neighbors of the current pixel. current_idx : int Index of highest-ranked pixel used as starting point in loop. image_stride : int - Stride between seed image and mask image in `ranks`. + Stride between seed image and mask image in `aranks`. """ cdef unsigned int neighbor_rank, current_rank, mask_rank - cdef int i, current_link, neighbor_idx, nprev, nnext - cdef int nstrides = strides.shape[0] + cdef int i, neighbor_idx, current_link, nprev, nnext + cdef int nstrides = astrides.shape[0] + cdef cnp.uint32_t *ranks = (aranks.data) + cdef cnp.int32_t *prev = (aprev.data) + cdef cnp.int32_t *next = (anext.data) + cdef cnp.int32_t *strides = (astrides.data) while current_idx != -1: if current_idx < image_stride: From 69207c003b1270e900acc248cc4aa740f121d080 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 25 Aug 2012 14:06:20 -0400 Subject: [PATCH 365/648] BUG: fix import of rank_order --- skimage/morphology/greyreconstruct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 9362e3fb..bf3caffe 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -11,7 +11,7 @@ Original author: Lee Kamentsky """ import numpy as np -from skimage.filter.rank_order import rank_order +from skimage.filter._rank_order import rank_order def reconstruction(seed, mask, method='dilation', selem=None, offset=None): From 76f75c8ab619978ae219ccf11c9340d8a98e3f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 25 Aug 2012 22:46:45 +0200 Subject: [PATCH 366/648] Fix C-contiguous array bug in match template --- skimage/feature/template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/feature/template.py b/skimage/feature/template.py index 500f02a0..19d22c9b 100644 --- a/skimage/feature/template.py +++ b/skimage/feature/template.py @@ -67,8 +67,8 @@ def match_template(image, template, pad_input=False): """ if np.any(np.less(image.shape, template.shape)): raise ValueError("Image must be larger than template.") - image = convert(image, np.float32) - template = convert(template, np.float32) + 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) From 32afefef2336eda736ff5f305b24927d7535b790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 00:08:29 +0200 Subject: [PATCH 367/648] Fix doc string of texture detection --- skimage/feature/_texture.pyx | 3 ++- skimage/feature/texture.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 0e82c306..5fb53e90 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -95,7 +95,8 @@ def _local_binary_pattern(np.ndarray[double, ndim=2] image, R : float Radius of circle (spatial resolution of the operator). method : {'D', 'R', 'U', 'V'} - Method to determine the pattern:: + Method to determine the pattern. + * 'D': 'default' * 'R': 'ror' * 'U': 'uniform' diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index aa970d18..22b5d0d8 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -139,7 +139,6 @@ def greycoprops(P, prop='contrast'): `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'. @@ -241,8 +240,9 @@ def local_binary_pattern(image, P, R, method='default'): angular space). R : float Radius of circle (spatial resolution of the operator). - method : {'D', 'R', 'U', 'V'} - Method to determine the pattern:: + method : {'default', 'ror', 'uniform', 'var'} + Method to determine the pattern. + * 'default': original local binary pattern which is gray scale but not rotation invariant. * 'ror': extension of default implementation which is gray scale and From 24d1b201c94cbbfb43d8fb747666d90a55c22e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 00:10:28 +0200 Subject: [PATCH 368/648] Delete unnecessary file --- skimage/draw/__init__.py | 3 ++- skimage/draw/draw.py | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 skimage/draw/draw.py diff --git a/skimage/draw/__init__.py b/skimage/draw/__init__.py index c6df1f73..4eac9b63 100644 --- a/skimage/draw/__init__.py +++ b/skimage/draw/__init__.py @@ -1 +1,2 @@ -from .draw import * +from ._draw import line, polygon, ellipse, circle +bresenham = line diff --git a/skimage/draw/draw.py b/skimage/draw/draw.py deleted file mode 100644 index 4b55b0de..00000000 --- a/skimage/draw/draw.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Methods to draw on arrays. - -""" - -from ._draw import line, polygon, ellipse, circle -bresenham = line From 4fe9f39c45ed1cc21848a27fc1ac14d68ab5d771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 00:16:52 +0200 Subject: [PATCH 369/648] Fix shape format of arrays in doc strings --- skimage/filter/thresholding.py | 4 ++-- skimage/transform/radon_transform.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 022cacc5..3f8acdef 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -16,7 +16,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, Parameters ---------- - image : NxM ndarray + image : (N, M) ndarray Input image. block_size : int Uneven size of pixel neighborhood which is used to calculate the @@ -45,7 +45,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, Returns ------- - threshold : NxM ndarray + threshold : (N, M) ndarray Thresholded binary image References diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index b716a6f1..f80cc3cb 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -100,7 +100,7 @@ def iradon(radon_image, theta=None, output_size=None, the image corresponds to a projection along a different angle. theta : array_like, dtype=float, optional Reconstruction angles (in degrees). Default: m angles evenly spaced - between 0 and 180 (if the shape of `radon_image` is nxm) + between 0 and 180 (if the shape of `radon_image` is (N, M)). output_size : int Number of rows and columns in the reconstruction. filter : str, optional (default ramp) From 7acb9f9efb89a3834e6f9476d209665b535cdb5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 00:27:55 +0200 Subject: [PATCH 370/648] Fix short description in doc string --- skimage/data/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index 7e351293..5e51d353 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -29,8 +29,9 @@ def load(f): def camera(): - """Gray-level "camera" image, often used for segmentation - and denoising examples. + """Gray-level "camera" image. + + Often used for segmentation and denoising examples. """ return load("camera.png") @@ -49,7 +50,7 @@ def lena(): def text(): - """ Gray-level "text" image used for corner detection. + """Gray-level "text" image used for corner detection. Notes ----- From e9b8645ee67e58fb322354f663744279b667dfa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 00:29:18 +0200 Subject: [PATCH 371/648] Fix parameter format in doc string of peak_local_max --- skimage/feature/peak.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/skimage/feature/peak.py b/skimage/feature/peak.py index 57eb3bb7..4765974e 100644 --- a/skimage/feature/peak.py +++ b/skimage/feature/peak.py @@ -15,21 +15,16 @@ def peak_local_max(image, min_distance=10, threshold='deprecated', Parameters ---------- - image: ndarray of floats + image : ndarray of floats Input image. - - min_distance: int + min_distance : int Minimum number of pixels separating peaks and image boundary. - threshold : float Deprecated. See `threshold_rel`. - - threshold_abs: float + threshold_abs : float Minimum intensity of peaks. - - threshold_rel: float + threshold_rel : float Minimum intensity of peaks calculated as `max(image) * threshold_rel`. - num_peaks : int Maximum number of peaks. When the number of peaks exceeds `num_peaks`, return `num_peaks` coordinates based on peak intensity. From 83a52f4d393ac1ffc3f951ad88e850c300e01ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 00:32:20 +0200 Subject: [PATCH 372/648] Capitalize parameter descriptions in draw package --- skimage/draw/_draw.pyx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index fbd525fa..ca0c502a 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -65,6 +65,7 @@ def line(int y, int x, int y2, int x2): return rr, cc + @cython.boundscheck(False) @cython.wraparound(False) @cython.nonecheck(False) @@ -74,9 +75,9 @@ def polygon(y, x, shape=None): Parameters ---------- y : (N,) ndarray - y coordinates of vertices of polygon + Y-coordinates of vertices of polygon. x : (N,) ndarray - x coordinates of vertices of polygon + X-coordinates of vertices of polygon. shape : tuple, optional image shape which is used to determine maximum extents of output pixel coordinates. This is useful for polygons which exceed the image size. @@ -121,6 +122,7 @@ def polygon(y, x, shape=None): return np.array(rr), np.array(cc) + @cython.boundscheck(False) @cython.wraparound(False) @cython.nonecheck(False) @@ -131,9 +133,9 @@ def ellipse(double cy, double cx, double b, double a, shape=None): Parameters ---------- cy, cx : double - centre coordinate of ellipse + Centre coordinate of ellipse. b, a: double - minor and major semi-axes. (x/a)**2 + (y/b)**2 = 1 + Minor and major semi-axes. ``(x/a)**2 + (y/b)**2 = 1``. Returns ------- @@ -166,20 +168,21 @@ def ellipse(double cy, double cx, double b, double a, shape=None): return np.array(rr), np.array(cc) + def circle(double cy, double cx, double radius, shape=None): """Generate coordinates of pixels within circle. Parameters ---------- cy, cx : double - centre coordinate of circle + Centre coordinate of circle. radius: double - radius of circle + Radius of circle. Returns ------- rr, cc : ndarray of int - Pixel coordinates of ellipse. + Pixel coordinates of circle. May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. """ From fc7859471f6101aba604ef3ee06262cc97b27f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 00:35:16 +0200 Subject: [PATCH 373/648] Fix examples of exposure package --- skimage/exposure/exposure.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 5a722308..b0ef45cf 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -135,30 +135,36 @@ def rescale_intensity(image, in_range=None, out_range=None): Examples -------- By default, intensities are stretched to the limits allowed by the dtype: + >>> image = np.array([51, 102, 153], dtype=np.uint8) >>> rescale_intensity(image) array([ 0, 127, 255], dtype=uint8) It's easy to accidentally convert an image dtype from uint8 to float: + >>> 1.0 * image array([ 51., 102., 153.]) Use `rescale_intensity` to rescale to the proper range for float dtypes: + >>> image_float = 1.0 * image >>> rescale_intensity(image_float) array([ 0. , 0.5, 1. ]) To maintain the low contrast of the original, use the `in_range` parameter: + >>> rescale_intensity(image_float, in_range=(0, 255)) array([ 0.2, 0.4, 0.6]) If the min/max value of `in_range` is more/less than the min/max image intensity, then the intensity levels are clipped: + >>> rescale_intensity(image_float, in_range=(0, 102)) array([ 0.5, 1. , 1. ]) If you have an image with signed integers but want to rescale the image to just the positive range, use the `out_range` parameter: + >>> image = np.array([-10, 0, 10], dtype=np.int8) >>> rescale_intensity(image, out_range=(0, 127)) array([ 0, 63, 127], dtype=int8) From db9af0dc2d59788c222ed7b4cd76d06cd1d21902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 08:25:04 +0200 Subject: [PATCH 374/648] Capitalize parameter descriptions in geometric transforms --- skimage/transform/_geometric.py | 50 ++++++++++++++++----------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index cec591a9..c3b47a3b 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -7,7 +7,7 @@ from skimage.util import img_as_float def _stackcopy(a, b): """Copy b into each color layer of a, such that:: - a[:,:,0] = a[:,:,1] = ... = b + a[:,:,0] = a[:,:,1] = ... = b Parameters ---------- @@ -37,12 +37,12 @@ class GeometricTransform(object): Parameters ---------- coords : (N, 2) array - source coordinates + Source coordinates. Returns ------- coords : (N, 2) array - transformed coordinates + Transformed coordinates. """ raise NotImplementedError() @@ -53,12 +53,12 @@ class GeometricTransform(object): Parameters ---------- coords : (N, 2) array - source coordinates + Source coordinates. Returns ------- coords : (N, 2) array - transformed coordinates + Transformed coordinates. """ raise NotImplementedError() @@ -182,9 +182,9 @@ class ProjectiveTransform(GeometricTransform): Parameters ---------- src : (N, 2) array - source coordinates + Source coordinates. dst : (N, 2) array - destination coordinates + Destination coordinates. """ xs = src[:, 0] @@ -260,13 +260,13 @@ class AffineTransform(ProjectiveTransform): matrix : (3, 3) array, optional Homogeneous transformation matrix. scale : (sx, sy) as array, list or tuple, optional - scale factors + Scale factors. rotation : float, optional - rotation angle in counter-clockwise direction as radians + Rotation angle in counter-clockwise direction as radians. shear : float, optional - shear angle in counter-clockwise direction as radians + Shear angle in counter-clockwise direction as radians. translation : (tx, ty) as array, list or tuple, optional - translation parameters + Translation parameters. """ @@ -345,11 +345,11 @@ class SimilarityTransform(ProjectiveTransform): matrix : (3, 3) array, optional Homogeneous transformation matrix. scale : float, optional - scale factor + Scale factor. rotation : float, optional - rotation angle in counter-clockwise direction as radians + Rotation angle in counter-clockwise direction as radians. translation : (tx, ty) as array, list or tuple, optional - x, y translation parameters + x, y translation parameters. """ @@ -420,9 +420,9 @@ class SimilarityTransform(ProjectiveTransform): Parameters ---------- src : (N, 2) array - source coordinates + Source coordinates. dst : (N, 2) array - destination coordinates + Destination coordinates. """ xs = src[:, 0] @@ -530,11 +530,11 @@ class PolynomialTransform(GeometricTransform): Parameters ---------- src : (N, 2) array - source coordinates + Source coordinates. dst : (N, 2) array - destination coordinates + Destination coordinates. order : int - polynomial order (number of coefficients is order + 1) + Polynomial order (number of coefficients is order + 1). """ xs = src[:, 0] @@ -576,7 +576,7 @@ class PolynomialTransform(GeometricTransform): Returns ------- coords : (N, 2) array - transformed coordinates + Transformed coordinates. """ x = coords[:, 0] @@ -692,7 +692,7 @@ def matrix_transform(coords, matrix): Returns ------- coords : (N, 2) array - transformed coordinates + Transformed coordinates. """ return ProjectiveTransform(matrix)(coords) @@ -705,13 +705,13 @@ def warp_coords(orows, ocols, bands, coord_transform_fn, Parameters ---------- orows : int - number of output rows + Number of output rows. ocols : int - number of output columns + Number of output columns. bands : int - number of color bands (aka channels) + Number of color bands (aka channels). coord_transform_fn : callable like GeometricTransform.inverse - Return input coordinates for given output coordinates + Return input coordinates for given output coordinates. dtype : np.dtype or string dtype for return value (sane choices: float32 or float64) From 00b3e90e42eb66824a110321509f85afe3e2f944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 08:37:51 +0200 Subject: [PATCH 375/648] Fix description of polygon example script --- doc/examples/plot_polygon.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/examples/plot_polygon.py b/doc/examples/plot_polygon.py index 6eaef0bf..5b745fed 100644 --- a/doc/examples/plot_polygon.py +++ b/doc/examples/plot_polygon.py @@ -1,10 +1,10 @@ """ -==================== -Approximate Polygons -==================== +=================================== +Approximatea and subdivide polygons +=================================== -This example shows how to approximate polygonal chains with the Douglas-Peucker -algorithm. +This example shows how to approximate (Douglas-Peucker algorithm) and subdivide +(B-Splines) polygonal chains. """ import numpy as np From 8084faf1f69c2c4ba79b6e85c7a2a06e1e7c77b8 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 26 Aug 2012 16:33:25 -0400 Subject: [PATCH 376/648] BUG: Fix division error for Python 3 --- skimage/morphology/greyreconstruct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index bf3caffe..9e447800 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -133,7 +133,7 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): if offset == None: if not all([d % 2 == 1 for d in selem.shape]): ValueError("Footprint dimensions must all be odd") - offset = np.array([d / 2 for d in selem.shape]) + offset = np.array([d // 2 for d in selem.shape]) # Cross out the center of the selem selem[[slice(d, d + 1) for d in offset]] = False From 7d8bc483787a2241e861b099a4ca28ceee1d8842 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 26 Aug 2012 16:36:28 -0400 Subject: [PATCH 377/648] ENH: Change assert statement for better error output --- skimage/morphology/tests/test_reconstruction.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/morphology/tests/test_reconstruction.py b/skimage/morphology/tests/test_reconstruction.py index 74f61b9f..8e40ac67 100644 --- a/skimage/morphology/tests/test_reconstruction.py +++ b/skimage/morphology/tests/test_reconstruction.py @@ -15,19 +15,19 @@ from skimage.morphology.greyreconstruct import reconstruction def test_zeros(): """Test reconstruction with image and mask of zeros""" - assert np.all(reconstruction(np.zeros((5, 7)), np.zeros((5, 7))) == 0) + assert_close(reconstruction(np.zeros((5, 7)), np.zeros((5, 7))), 0) def test_image_equals_mask(): """Test reconstruction where the image and mask are the same""" - assert np.all(reconstruction(np.ones((7, 5)), np.ones((7, 5))) == 1) + assert_close(reconstruction(np.ones((7, 5)), np.ones((7, 5))), 1) def test_image_less_than_mask(): """Test reconstruction where the image is uniform and less than mask""" image = np.ones((5, 5)) mask = np.ones((5, 5)) * 2 - assert np.all(reconstruction(image, mask) == 1) + assert_close(reconstruction(image, mask), 1) def test_one_image_peak(): @@ -35,7 +35,7 @@ def test_one_image_peak(): image = np.ones((5, 5)) image[2, 2] = 2 mask = np.ones((5, 5)) * 3 - assert np.all(reconstruction(image, mask) == 2) + assert_close(reconstruction(image, mask), 2) def test_two_image_peaks(): @@ -60,13 +60,13 @@ def test_two_image_peaks(): [1, 1, 1, 1, 1, 3, 3, 3], [1, 1, 1, 1, 1, 3, 3, 3], [1, 1, 1, 1, 1, 3, 3, 3]]) - assert np.all(reconstruction(image, mask) == expected) + assert_close(reconstruction(image, mask), expected) def test_zero_image_one_mask(): """Test reconstruction with an image of all zeros and a mask that's not""" result = reconstruction(np.zeros((10, 10)), np.ones((10, 10))) - assert np.all(result == 0) + assert_close(result, 0) def test_fill_hole(): From 9d29d5df78586ea2cc54cb7c023787a989512724 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 27 Aug 2012 11:26:11 +0200 Subject: [PATCH 378/648] PEP8: indentation in random_walker_segmentation --- .../segmentation/random_walker_segmentation.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 93cba2a7..d5e8e9f2 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -135,15 +135,15 @@ def _mask_edges_weights(edges, weights, mask): corresponding weights of the edges. """ mask0 = np.hstack((mask[:, :, :-1].ravel(), mask[:, :-1].ravel(), - mask[:-1].ravel())) + mask[:-1].ravel())) mask1 = np.hstack((mask[:, :, 1:].ravel(), mask[:, 1:].ravel(), - mask[1:].ravel())) + mask[1:].ravel())) ind_mask = np.logical_and(mask0, mask1) edges, weights = edges[:, ind_mask], weights[ind_mask] max_node_index = edges.max() # Reassign edges labels to 0, 1, ... edges_number - 1 order = np.searchsorted(np.unique(edges.ravel()), - np.arange(max_node_index + 1)) + np.arange(max_node_index + 1)) edges = order[edges] return edges, weights @@ -163,7 +163,7 @@ def _build_laplacian(data, mask=None, beta=50): def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, - return_full_prob=False): + return_full_prob=False): """ Random walker algorithm for segmentation from markers. @@ -306,7 +306,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, labels = np.copy(labels) label_values = np.unique(labels) # Reorder label values to have consecutive integers (no gaps) - if np.any(np.diff(label_values) > 1): + if np.any(np.diff(label_values) != 1): mask = labels >= 0 labels[mask] = rank_order(labels[mask])[0].astype(labels.dtype) labels = labels.astype(np.int32) @@ -328,7 +328,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, # first at pixel j by anisotropic diffusion. if mode == 'cg': X = _solve_cg(lap_sparse, B, tol=tol, - return_full_prob=return_full_prob) + return_full_prob=return_full_prob) if mode == 'cg_mg': if not amg_loaded: warnings.warn( @@ -338,10 +338,10 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, X = _solve_cg(lap_sparse, B, tol=tol) else: X = _solve_cg_mg(lap_sparse, B, tol=tol, - return_full_prob=return_full_prob) + return_full_prob=return_full_prob) if mode == 'bf': X = _solve_bf(lap_sparse, B, - return_full_prob=return_full_prob) + return_full_prob=return_full_prob) # Clean up results data = np.squeeze(data) if return_full_prob: @@ -352,7 +352,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, mask_i = np.squeeze(labels == i) X[i - 1, mask_i] = 1 X[np.setdiff1d(np.arange(0, labels.max(), dtype=np.int), - [i - 1]), mask_i] = 0 + [i - 1]), mask_i] = 0 else: X = _clean_labels_ar(X + 1, labels).reshape(data.shape) return X From 06440262505e7d7cf8eb3fb787304b58222c9f94 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 27 Aug 2012 12:04:30 +0200 Subject: [PATCH 379/648] Fixed two tests that were failing because of draw imports --- skimage/feature/tests/test_hog.py | 2 +- skimage/morphology/tests/test_skeletonize.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/feature/tests/test_hog.py b/skimage/feature/tests/test_hog.py index c9449028..6f2d4cdf 100644 --- a/skimage/feature/tests/test_hog.py +++ b/skimage/feature/tests/test_hog.py @@ -3,7 +3,7 @@ from scipy import ndimage from skimage import data from skimage import feature from skimage import img_as_float -from skimage.draw import draw +from skimage import draw from numpy.testing import * def test_histogram_of_oriented_gradients(): diff --git a/skimage/morphology/tests/test_skeletonize.py b/skimage/morphology/tests/test_skeletonize.py index c9cdbc24..709e1ba4 100644 --- a/skimage/morphology/tests/test_skeletonize.py +++ b/skimage/morphology/tests/test_skeletonize.py @@ -1,7 +1,7 @@ import numpy as np from skimage.morphology import skeletonize, medial_axis import numpy.testing -from skimage.draw import draw +from skimage import draw from scipy.ndimage import correlate from skimage.io import imread from skimage import data_dir From bc1582ced28877d104a6a397b0a6ff59f4e4fb0f Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 27 Aug 2012 12:18:14 +0200 Subject: [PATCH 380/648] acronym --> full name of algorithm in example --- doc/examples/plot_segmentations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/examples/plot_segmentations.py b/doc/examples/plot_segmentations.py index a8ee7cea..99bfefcc 100644 --- a/doc/examples/plot_segmentations.py +++ b/doc/examples/plot_segmentations.py @@ -7,7 +7,8 @@ This example compares three popular low-level image segmentation methods. As it is difficult to obtain good segmentations, and the definition of "good" often depends on the application, these methods are usually used for obtaining an oversegmentation, also known as superpixels. These superpixels then serve as -a basis for more sophisticated algorithms such as CRFs. +a basis for more sophisticated algorithms such as conditional random fields +(CRF). Felzenszwalb's efficient graph based segmentation From 14d0923959e7509fd40f3f1b67e43989fa72a037 Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Mon, 27 Aug 2012 12:41:29 +0200 Subject: [PATCH 381/648] fixed data of tv_denoise result images with float datatype to be in default range [0.0:1.0] --- skimage/filter/_tv_denoise.py | 27 ++++++++++++++++++++++++- skimage/filter/tests/test_tv_denoise.py | 11 +++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/skimage/filter/_tv_denoise.py b/skimage/filter/_tv_denoise.py index e8736786..af8decfc 100644 --- a/skimage/filter/_tv_denoise.py +++ b/skimage/filter/_tv_denoise.py @@ -170,7 +170,30 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): i += 1 return out +def _renormalize_image(image, data_type): + """ + inplace renormalize image date to obey the float range [0.0:1.0] + Parameters + ---------- + immage: ndarray + input data to be renormalized + + data_type: type, + image data type before running tv_denoise + + Notes + ------- + the minimum and maximum values of the image data type define the range + which is mapped to the interval [0.0:1.0] + + """ + type_info = np.iinfo(data_type) + start = type_info.min + width = type_info.max - type_info.min + np.subtract(image, start, image) + np.divide(image, width, image) + def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): """ Perform total-variation denoising on 2-d and 3-d images @@ -257,4 +280,6 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): if keep_type: return out.astype(im_type) else: - return out + if not im_type.kind == 'f': + _renormalize_image(out, im_type) + return out diff --git a/skimage/filter/tests/test_tv_denoise.py b/skimage/filter/tests/test_tv_denoise.py index 4cc6adbb..47f53251 100644 --- a/skimage/filter/tests/test_tv_denoise.py +++ b/skimage/filter/tests/test_tv_denoise.py @@ -33,6 +33,16 @@ class TestTvDenoise(): weight=60.0, keep_type=True) assert denoised_lena_int.dtype is np.dtype('uint16') + def test_tv_denoise_float_result_range(self): + # lena image + lena = color.rgb2gray(data.lena())[:256, :256] + int_lena = np.multiply(lena, 255).astype(np.uint8) + assert np.max(int_lena) > 1 + denoised_int_lena = filter.tv_denoise(int_lena, weight=60.0) + # test if the value range of output float data is within [0.0:1.0] + assert np.max(denoised_int_lena) <= 1.0 + assert np.min(denoised_int_lena) >= 0.0 + def test_tv_denoise_3d(self): """ Apply the TV denoising algorithm on a 3D image representing @@ -59,6 +69,5 @@ class TestTvDenoise(): except ValueError: pass - if __name__ == "__main__": run_module_suite() From 0293403b685c379944bd986ff925a85b6f949f26 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 27 Aug 2012 12:57:24 +0200 Subject: [PATCH 382/648] DOC: example in slic segmentation docstring --- skimage/segmentation/_slic.pyx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index ecb58efe..b6831e80 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -14,6 +14,8 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, ---------- image : (width, height, 3) ndarray Input image. + n_segments : int + The (approximate) number of labels in the segmented output image. ratio: float Balances color-space proximity and image-space proximity. Higher values give more weight to color-space. @@ -42,6 +44,15 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, Pascal Fua, and Sabine Süsstrunk, SLIC Superpixels Compared to State-of-the-art Superpixel Methods, TPAMI, May 2012. + Examples + -------- + >>> from skimage.segmentation import slic + >>> from skimage.data import lena + >>> from skimage.util import img_as_float + >>> img = lena() + >>> segments = slic(img, n_segments=100, ratio=10) + >>> # Increasing the ratio parameter yields more square regions + >>> segments = slic(img, n_segments=100, ratio=20) """ image = np.atleast_3d(image) if image.shape[2] != 3: From 77f1e0ba473240fb9fcccd5d9ec884b9556b63c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 08:52:20 +0200 Subject: [PATCH 383/648] Add deprecation warning in doc string to homography --- skimage/transform/_project.pyx | 3 +-- skimage/transform/_warps.py | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/skimage/transform/_project.pyx b/skimage/transform/_project.pyx index 4df6e001..201785ad 100644 --- a/skimage/transform/_project.pyx +++ b/skimage/transform/_project.pyx @@ -35,8 +35,7 @@ cdef inline _matrix_transform(double x, double y, double* H, double *x_, def homography(np.ndarray image, np.ndarray H, output_shape=None, int order=1, mode='constant', double cval=0): - """ - Projective transformation (homography). + """Projective transformation (homography). Perform a projective transformation (homography) of a floating point image, using bi-linear interpolation. diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index f09f7944..4c499d6b 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -74,7 +74,11 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, def homography(image, H, output_shape=None, order=1, mode='constant', cval=0.): - """Perform a projective transformation (homography) on an image. + """ + .. deprecated:: + 0.7 + + Perform a projective transformation (homography) on an image. For each pixel, given its homogeneous coordinate :math:`\mathbf{x} = [x, y, 1]^T`, its target position is calculated by multiplying From a6532a8dae3f7e4a211a8f5825a4110f0cf5dfd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 15:39:48 +0200 Subject: [PATCH 384/648] Refactor image warps * Fix cval bug in interpolation which was ignored * Remove fast_homography as standalone function and automatically include functionality in warp * Fix bug in warp_coords for graylevel images * move warp functions to warp file --- skimage/_shared/interpolation.pyx | 18 +- skimage/transform/__init__.py | 5 +- skimage/transform/_geometric.py | 186 +-------------- skimage/transform/_warps.py | 212 +++++++++++++++++- .../transform/{_project.pyx => _warps_cy.pyx} | 18 +- skimage/transform/setup.py | 4 +- skimage/transform/tests/test_warps.py | 16 +- 7 files changed, 245 insertions(+), 214 deletions(-) rename skimage/transform/{_project.pyx => _warps_cy.pyx} (90%) diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx index 137004e5..83722aba 100644 --- a/skimage/_shared/interpolation.pyx +++ b/skimage/_shared/interpolation.pyx @@ -18,8 +18,8 @@ cdef inline double nearest_neighbour(double* image, int rows, int cols, Shape of image. r, c : int Position at which to interpolate. - mode : {'C', 'W', 'M'} - Wrapping mode. Constant, Wrap or Mirror. + mode : {'C', 'W', 'R'} + Wrapping mode. Constant, Wrap or Reflect. cval : double Constant value to use for constant mode. @@ -42,8 +42,8 @@ cdef inline double bilinear_interpolation(double* image, int rows, int cols, Shape of image. r, c : int Position at which to interpolate. - mode : {'C', 'W', 'M'} - Wrapping mode. Constant, Wrap or Mirror. + mode : {'C', 'W', 'R'} + Wrapping mode. Constant, Wrap or Reflect. cval : double Constant value to use for constant mode. @@ -76,8 +76,8 @@ cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, Shape of image. r, c : int Position at which to get the pixel. - mode : {'C', 'W', 'M'} - Wrapping mode. Constant, Wrap or Mirror. + mode : {'C', 'W', 'R'} + Wrapping mode. Constant, Wrap or Reflect. cval : double Constant value to use for constant mode. @@ -101,13 +101,13 @@ cdef inline int coord_map(int dim, int coord, char mode): Maximum coordinate. coord : int Coord provided by user. May be < 0 or > dim. - mode : {'W', 'M'} - Whether to wrap or mirror the coordinate if it + mode : {'W', 'R'} + Whether to wrap or reflect the coordinate if it falls outside [0, dim). """ dim = dim - 1 - if mode == 'M': # mirror + if mode == 'R': # reflect if (coord < 0): # How many times times does the coordinate wrap? if ((-coord / dim) % 2 != 0): diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index b0eee512..0735267a 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -1,9 +1,8 @@ from .hough_transform import * from .radon_transform import * from .finite_radon_transform import * -from ._project import homography as fast_homography from .integral import * -from ._geometric import (warp, warp_coords, estimate_transform, +from ._geometric import (estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform) -from ._warps import swirl, homography +from ._warps import warp, warp_coords, swirl, homography diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index c3b47a3b..e4799375 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1,30 +1,5 @@ import math import numpy as np -from scipy import ndimage -from skimage.util import img_as_float - - -def _stackcopy(a, b): - """Copy b into each color layer of a, such that:: - - a[:,:,0] = a[:,:,1] = ... = b - - Parameters - ---------- - a : (M, N) or (M, N, P) ndarray - Target array. - b : (M, N) - Source array. - - Notes - ----- - Color images are stored as an ``(M, N, 3)`` or ``(M, N, 4)`` arrays. - - """ - if a.ndim == 3: - a[:] = b[:, :, np.newaxis] - else: - a[:] = b class GeometricTransform(object): @@ -603,7 +578,7 @@ class PolynomialTransform(GeometricTransform): 'then apply the forward transformation.') -TRANSFORMATIONS = { +TRANSFORMS = { 'similarity': SimilarityTransform, 'affine': AffineTransform, 'projective': ProjectiveTransform, @@ -669,11 +644,11 @@ def estimate_transform(ttype, src, dst, **kwargs): """ ttype = ttype.lower() - if ttype not in TRANSFORMATIONS: + if ttype not in TRANSFORMS: raise ValueError('the transformation type \'%s\' is not' 'implemented' % ttype) - tform = TRANSFORMATIONS[ttype]() + tform = TRANSFORMS[ttype]() tform.estimate(src, dst, **kwargs) return tform @@ -696,158 +671,3 @@ def matrix_transform(coords, matrix): """ return ProjectiveTransform(matrix)(coords) - - -def warp_coords(orows, ocols, bands, coord_transform_fn, - dtype=np.float64): - """Build the source coordinates for the output pixels of an image warp. - - Parameters - ---------- - orows : int - Number of output rows. - ocols : int - Number of output columns. - bands : int - Number of color bands (aka channels). - coord_transform_fn : callable like GeometricTransform.inverse - Return input coordinates for given output coordinates. - dtype : np.dtype or string - dtype for return value (sane choices: float32 or float64) - - Returns - ------- - coords : (3, orows, ocols, bands) array of dtype `dtype` - Coordinates for `scipy.ndimage.map_coordinates`, that will yield - an image of shape (orows, ocols, bands) by drawing from source - points according to the `coord_transform_fn`. - - Notes - ----- - This is a lower-level routine that produces the source coordinates used by - `warp()`. - - It is provided separately from `warp` to give additional flexibility to - users who would like, for example, to re-use a particular coordinate - mapping, to use specific dtypes at various points along the the - image-warping process, or to implement different post-processing logic - than `warp` performs after the call to `ndimage.map_coordinates`. - - - Examples - -------- - Produce a coordinate map that Shifts an image to the right: - - >>> from skimage import data - >>> from scipy.ndimage import map_coordinates - >>> - >>> def shift_right(xy): - ... xy[:, 0] -= 10 - ... return xy - >>> - >>> coords = warp_coords(30, 30, 3, shift_right) - >>> image = data.lena().astype(np.float32) - >>> warped_image = map_coordinates(image, coords) - - """ - - coords = np.empty((3, orows, ocols, bands), dtype=dtype) - - # Reshape grid coordinates into a (P, 2) array of (x, y) pairs - tf_coords = np.indices((ocols, orows), dtype=dtype).reshape(2, -1).T - - # Map each (x, y) pair to the source image according to - # the user-provided mapping - tf_coords = coord_transform_fn(tf_coords) - - # Reshape back to a (2, M, N) coordinate grid - tf_coords = tf_coords.T.reshape((-1, ocols, orows)).swapaxes(1, 2) - - # Place the y-coordinate mapping - _stackcopy(coords[1, ...], tf_coords[0, ...]) - - # Place the x-coordinate mapping - _stackcopy(coords[0, ...], tf_coords[1, ...]) - - # colour-coordinate mapping - coords[2, ...] = range(bands) - - return coords - - -def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, - mode='constant', cval=0., reverse_map=None): - """Warp an image according to a given coordinate transformation. - - Parameters - ---------- - image : 2-D array - Input image. - inverse_map : transformation object, callable xy = f(xy, **kwargs) - Inverse coordinate map. A function that transforms a (N, 2) array of - ``(x, y)`` coordinates in the *output image* into their corresponding - coordinates in the *source image* (e.g. a transformation object or its - inverse). - map_args : dict, optional - Keyword arguments passed to `inverse_map`. - output_shape : tuple (rows, cols) - Shape of the output image generated. - order : int - Order of splines used in interpolation. See - `scipy.ndimage.map_coordinates` for detail. - mode : string - How to handle values outside the image borders. See - `scipy.ndimage.map_coordinates` for detail. - cval : float - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - Examples - -------- - Shift an image to the right: - - >>> from skimage import data - >>> image = data.camera() - >>> - >>> def shift_right(xy): - ... xy[:, 0] -= 10 - ... return xy - >>> - >>> warp(image, shift_right) - - """ - # Backward API compatibility - if reverse_map is not None: - inverse_map = reverse_map - - if image.ndim < 2: - raise ValueError("Input must have more than 1 dimension.") - - image = np.atleast_3d(img_as_float(image)) - ishape = np.array(image.shape) - bands = ishape[2] - - if output_shape is None: - output_shape = ishape - - rows, cols = output_shape[:2] - - def coord_transform_fn(*args): - return inverse_map(*args, **map_args) - - coords = warp_coords(rows, cols, bands, coord_transform_fn) - - # Prefilter not necessary for order 1 interpolation - prefilter = order > 1 - mapped = ndimage.map_coordinates(image, coords, prefilter=prefilter, - mode=mode, order=order, cval=cval) - - # The spline filters sometimes return results outside [0, 1], - # so clip to ensure valid data - clipped = np.clip(mapped, 0, 1) - - if mode == 'constant' and not (0 <= cval <= 1): - clipped[mapped == cval] = cval - - # Remove singleton dim introduced by atleast_3d - return clipped.squeeze() diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 4c499d6b..db7e5eba 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -1,5 +1,215 @@ -from ._geometric import warp, ProjectiveTransform import numpy as np +from scipy import ndimage +from skimage.util import img_as_float +from ._geometric import (SimilarityTransform, AffineTransform, + ProjectiveTransform) +from ._warps_cy import _warp_fast + + +HOMOGRAPHY_TRANSFORMS = ( + SimilarityTransform, + AffineTransform, + ProjectiveTransform +) + + +def _stackcopy(a, b): + """Copy b into each color layer of a, such that:: + + a[:,:,0] = a[:,:,1] = ... = b + + Parameters + ---------- + a : (M, N) or (M, N, P) ndarray + Target array. + b : (M, N) + Source array. + + Notes + ----- + Color images are stored as an ``(M, N, 3)`` or ``(M, N, 4)`` arrays. + + """ + if a.ndim == 3: + a[:] = b[:, :, np.newaxis] + else: + a[:] = b + + +def warp_coords(coord_map, shape, dtype=np.float64): + """Build the source coordinates for the output pixels of an image warp. + + Parameters + ---------- + coord_map : callable like GeometricTransform.inverse + Return input coordinates for given output coordinates. + shape : tuple + Shape of output image ``(rows, cols[, bands])``. + dtype : np.dtype or string + dtype for return value (sane choices: float32 or float64). + + Returns + ------- + coords : (ndim, rows, cols[, bands]) array of dtype `dtype` + Coordinates for `scipy.ndimage.map_coordinates`, that will yield + an image of shape (orows, ocols, bands) by drawing from source + points according to the `coord_transform_fn`. + + Notes + ----- + This is a lower-level routine that produces the source coordinates used by + `warp()`. + + It is provided separately from `warp` to give additional flexibility to + users who would like, for example, to re-use a particular coordinate + mapping, to use specific dtypes at various points along the the + image-warping process, or to implement different post-processing logic + than `warp` performs after the call to `ndimage.map_coordinates`. + + + Examples + -------- + Produce a coordinate map that Shifts an image to the right: + + >>> from skimage import data + >>> from scipy.ndimage import map_coordinates + >>> + >>> def shift_right(xy): + ... xy[:, 0] -= 10 + ... return xy + >>> + >>> coords = warp_coords(30, 30, 3, shift_right) + >>> image = data.lena().astype(np.float32) + >>> warped_image = map_coordinates(image, coords) + + """ + rows, cols = shape[0], shape[1] + coords_shape = [len(shape), rows, cols] + if len(shape) == 3: + coords_shape.append(shape[2]) + coords = np.empty(coords_shape, dtype=dtype) + + # Reshape grid coordinates into a (P, 2) array of (x, y) pairs + tf_coords = np.indices((cols, rows), dtype=dtype).reshape(2, -1).T + + # Map each (x, y) pair to the source image according to + # the user-provided mapping + tf_coords = coord_map(tf_coords) + + # Reshape back to a (2, M, N) coordinate grid + tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2) + + # Place the y-coordinate mapping + _stackcopy(coords[1, ...], tf_coords[0, ...]) + + # Place the x-coordinate mapping + _stackcopy(coords[0, ...], tf_coords[1, ...]) + + if len(shape) == 3: + coords[2, ...] = range(shape[2]) + + return coords + + +def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, + mode='constant', cval=0., reverse_map=None): + """Warp an image according to a given coordinate transformation. + + Parameters + ---------- + image : 2-D array + Input image. + inverse_map : transformation object, callable xy = f(xy, **kwargs) + Inverse coordinate map. A function that transforms a (N, 2) array of + ``(x, y)`` coordinates in the *output image* into their corresponding + coordinates in the *source image* (e.g. a transformation object or its + inverse). + map_args : dict, optional + Keyword arguments passed to `inverse_map`. + output_shape : tuple (rows, cols) + Shape of the output image generated. + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : float + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Examples + -------- + Shift an image to the right: + + >>> from skimage import data + >>> image = data.camera() + >>> + >>> def shift_right(xy): + ... xy[:, 0] -= 10 + ... return xy + >>> + >>> warp(image, shift_right) + + """ + # Backward API compatibility + if reverse_map is not None: + inverse_map = reverse_map + + if image.ndim < 2: + raise ValueError("Input must have more than 1 dimension.") + + orig_ndim = image.ndim + image = np.atleast_3d(img_as_float(image)) + ishape = np.array(image.shape) + bands = ishape[2] + + # use fast Cython version for specific parameters + fast_modes = ('constant', 'reflect', 'wrap') + if order in (0, 1) and mode in fast_modes and not map_args: + matrix = None + if isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): + matrix = inverse_map._matrix + elif inverse_map.__name__ == 'inverse' \ + and inverse_map.im_class in HOMOGRAPHY_TRANSFORMS: + matrix = np.linalg.inv(inverse_map.im_self._matrix) + if matrix is not None: + # transform all bands + dims = [] + for dim in range(image.shape[2]): + dims.append(_warp_fast(image[..., dim], matrix, + output_shape=output_shape, + order=order, mode=mode, cval=cval)) + out = np.dstack(dims) + if orig_ndim == 2: + out = out[..., 0] + return out + + if output_shape is None: + output_shape = ishape + + rows, cols = output_shape[:2] + + def coord_map(*args): + return inverse_map(*args, **map_args) + + coords = warp_coords(coord_map, (rows, cols, bands)) + + # Prefilter not necessary for order 1 interpolation + prefilter = order > 1 + mapped = ndimage.map_coordinates(image, coords, prefilter=prefilter, + mode=mode, order=order, cval=cval) + + # The spline filters sometimes return results outside [0, 1], + # so clip to ensure valid data + clipped = np.clip(mapped, 0, 1) + + if mode == 'constant' and not (0 <= cval <= 1): + clipped[mapped == cval] = cval + + # Remove singleton dim introduced by atleast_3d + return clipped.squeeze() + def _swirl_mapping(xy, center, rotation, strength, radius): x, y = xy.T diff --git a/skimage/transform/_project.pyx b/skimage/transform/_warps_cy.pyx similarity index 90% rename from skimage/transform/_project.pyx rename to skimage/transform/_warps_cy.pyx index 201785ad..68ccef27 100644 --- a/skimage/transform/_project.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -33,7 +33,7 @@ cdef inline _matrix_transform(double x, double y, double* H, double *x_, y_[0] = yy / zz -def homography(np.ndarray image, np.ndarray H, output_shape=None, int order=1, +def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, mode='constant', double cval=0): """Projective transformation (homography). @@ -71,7 +71,7 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, int order=1, Order of interpolation:: * 0: Nearest-neighbour interpolation. * 1: Bilinear interpolation (default). - mode : {'constant', 'mirror', 'wrap'} + mode : {'constant', 'reflect', 'wrap'} How to handle values outside the image borders. cval : string Used in conjunction with mode 'C' (constant), the value @@ -82,18 +82,18 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, int order=1, cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] img = \ np.ascontiguousarray(image, dtype=np.double) cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] M = \ - np.ascontiguousarray(np.linalg.inv(H)) + np.ascontiguousarray(H) - if mode not in ('constant', 'wrap', 'mirror'): + if mode not in ('constant', 'wrap', 'reflect'): raise ValueError("Invalid mode specified. Please use " - "`constant`, `wrap` or `mirror`.") + "`constant`, `wrap` or `reflect`.") cdef char mode_c if mode == 'constant': mode_c = ord('C') elif mode == 'wrap': mode_c = ord('W') - elif mode == 'mirror': - mode_c = ord('M') + elif mode == 'reflect': + mode_c = ord('R') cdef int out_r, out_c if output_shape is None: @@ -116,9 +116,9 @@ def homography(np.ndarray image, np.ndarray H, output_shape=None, int order=1, _matrix_transform(tfc, tfr, M.data, &c, &r) if order == 0: out[tfr, tfc] = nearest_neighbour(img.data, rows, - cols, r, c, mode_c) + cols, r, c, mode_c, cval) elif order == 1: out[tfr, tfc] = bilinear_interpolation(img.data, rows, - cols, r, c, mode_c) + cols, r, c, mode_c, cval) return out diff --git a/skimage/transform/setup.py b/skimage/transform/setup.py index 75210b54..0e415bad 100644 --- a/skimage/transform/setup.py +++ b/skimage/transform/setup.py @@ -14,12 +14,12 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('tests') cython(['_hough_transform.pyx'], working_path=base_path) - cython(['_project.pyx'], working_path=base_path) + cython(['_warps_cy.pyx'], working_path=base_path) config.add_extension('_hough_transform', sources=['_hough_transform.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_project', sources=['_project.c'], + config.add_extension('_warps_cy', sources=['_warps_cy.c'], include_dirs=[get_numpy_include_dirs(), '../_shared']) return config diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 7be3151b..f896a452 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -2,7 +2,7 @@ from numpy.testing import assert_array_almost_equal, run_module_suite import numpy as np from scipy.ndimage import map_coordinates -from skimage.transform import (warp, warp_coords, fast_homography, +from skimage.transform import (warp, warp_coords, AffineTransform, ProjectiveTransform, SimilarityTransform) @@ -55,11 +55,12 @@ def test_fast_homography(): H[:2, 2] = [tx, ty] tform = ProjectiveTransform(H) + coords = warp_coords(tform.inverse, (img.shape[0], img.shape[1])) for order in range(2): - for mode in ('constant', 'mirror', 'wrap'): - p0 = warp(img, tform.inverse, mode=mode, order=order) - p1 = fast_homography(img, H, mode=mode, order=order) + for mode in ('constant', 'reflect', 'wrap'): + p0 = map_coordinates(img, coords, mode=mode, order=order) + p1 = warp(img, tform, mode=mode, order=order) # import matplotlib.pyplot as plt # f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4) @@ -85,8 +86,9 @@ def test_swirl(): def test_const_cval_out_of_range(): img = np.random.randn(100, 100) - warped = warp(img, AffineTransform(translation=(10, 10)), cval=-10) - assert np.sum(warped < 0) == (2 * 100 * 10 - 10 * 10) + cval = - 10 + warped = warp(img, AffineTransform(translation=(10, 10)), cval=cval) + assert np.sum(warped == cval) == (2 * 100 * 10 - 10 * 10) def test_warp_identity(): @@ -107,7 +109,7 @@ def test_warp_coords_example(): image = data.lena().astype(np.float32) assert 3 == image.shape[2] tform = SimilarityTransform(translation=(0, -10)) - coords = warp_coords(30, 30, 3, tform) + coords = warp_coords(tform, (30, 30, 3)) warped_image1 = map_coordinates(image[:, :, 0], coords[:2]) From cfea01b9fffba5f1b217bdae92865102373e6d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 15:51:09 +0200 Subject: [PATCH 385/648] Fix import error in geometric test cases --- skimage/transform/tests/test_geometric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index abe8b2e7..f035631d 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -1,7 +1,7 @@ import numpy as np from numpy.testing import assert_equal, assert_array_almost_equal -from skimage.transform._geometric import _stackcopy +from skimage.transform._warps import _stackcopy from skimage.transform import (estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform) From 772a1cb4b0929093119ca166ac79b4eeb2856392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 17:31:22 +0200 Subject: [PATCH 386/648] Update radon transform with new warp function --- skimage/transform/radon_transform.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index f80cc3cb..0213b2bc 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -15,7 +15,7 @@ References: from __future__ import division import numpy as np from scipy.fftpack import fftshift, fft, ifft -from ._project import homography +from ._warps_cy import _warp_fast __all__ = ["radon", "iradon"] @@ -77,8 +77,8 @@ def radon(image, theta=None): return shift1.dot(R).dot(shift0) for i in range(len(theta)): - rotated = homography(padded_image, - build_rotation(-theta[i])) + rotated = _warp_fast(padded_image, + np.linalg.inv(build_rotation(-theta[i]))) out[:, i] = rotated.sum(0)[::-1] From a0649791aed1f2a75657cd94c29fdd4f5119c57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 17:33:23 +0200 Subject: [PATCH 387/648] Fix doc string example of warp_coords --- skimage/transform/_warps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index db7e5eba..e796b454 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -78,8 +78,8 @@ def warp_coords(coord_map, shape, dtype=np.float64): ... xy[:, 0] -= 10 ... return xy >>> - >>> coords = warp_coords(30, 30, 3, shift_right) >>> image = data.lena().astype(np.float32) + >>> coords = warp_coords(shift_right, image.shape) >>> warped_image = map_coordinates(image, coords) """ From 6f4c2ad2681e4c9b917a26dbe4305a4ef5ceedf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 18:46:13 +0200 Subject: [PATCH 388/648] Add function for image rotation --- skimage/transform/_warps.py | 62 +++++++++++++++++++++++++++ skimage/transform/tests/test_warps.py | 9 +++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index e796b454..7b0fbeed 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -211,6 +211,68 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, return clipped.squeeze() +def rotate(image, angle, preserve_shape=False, order=1, + mode='constant', cval=0.): + """Rotate image by a certain angle around its center. + + Parameters + ---------- + image : ndarray + Input image. + angle : float + Rotation angle in degrees in counter-clockwise direction. + preserve_shape : bool, optional + Determine whether the shape of the output image will be automatically + calculated, so the complete rotated image exactly fits. Default is + False. + + Returns + ------- + rotated : ndarray + Rotated version of the input. + + Other parameters + ---------------- + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + """ + + rows, cols = image.shape[0], image.shape[1] + + # rotation around center + translation = np.array((cols, rows)) / 2. + tform1 = SimilarityTransform(translation=-translation) + tform2 = SimilarityTransform(rotation=np.deg2rad(angle)) + tform3 = SimilarityTransform(translation=translation) + tform = tform1 + tform2 + tform3 + + output_shape = None + if not preserve_shape: + # determine shape of output image + corners = tform([[0, 0], [0, rows], [cols, 0], [cols, rows]]) + corners = np.round(corners, 4) + minc = np.floor(corners[:, 0].min()) + minr = np.floor(corners[:, 1].min()) + maxc = np.ceil(corners[:, 0].max()) + maxr = np.ceil(corners[:, 1].max()) + output_shape = [maxr - minr, maxc - minc] + + # fit output image in new shape + tform4 = SimilarityTransform(translation=(minc, minr + 1)) + tform = tform4 + tform + + return warp(image, tform, output_shape=output_shape, order=order, + mode=mode, cval=cval) + + def _swirl_mapping(xy, center, rotation, strength, radius): x, y = xy.T x0, y0 = center diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index f896a452..d5ffff59 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -2,7 +2,7 @@ from numpy.testing import assert_array_almost_equal, run_module_suite import numpy as np from scipy.ndimage import map_coordinates -from skimage.transform import (warp, warp_coords, +from skimage.transform import (warp, warp_coords, rotate, AffineTransform, ProjectiveTransform, SimilarityTransform) @@ -74,6 +74,13 @@ def test_fast_homography(): assert d < 0.001 +def test_rotate(): + x = np.zeros((5, 5), dtype=np.double) + x[1, 1] = 1 + x90 = rotate(x, 90) + assert_array_almost_equal(x90, np.rot90(x)) + + def test_swirl(): image = img_as_float(data.checkerboard()) From 6ec4c21cf7af09401aabadff79898fe783efe9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 22:55:35 +0200 Subject: [PATCH 389/648] Fix import of rotate function --- skimage/transform/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 0735267a..1ee519f2 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -5,4 +5,4 @@ from .integral import * from ._geometric import (estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform) -from ._warps import warp, warp_coords, swirl, homography +from ._warps import warp, warp_coords, rotate, swirl, homography From 00e1d14294ff9c11e06f49e8d366b4b60ad35493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 26 Aug 2012 22:56:57 +0200 Subject: [PATCH 390/648] Fix rotate translation bug --- skimage/transform/_warps.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 7b0fbeed..e9cb61e5 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -248,7 +248,7 @@ def rotate(image, angle, preserve_shape=False, order=1, rows, cols = image.shape[0], image.shape[1] # rotation around center - translation = np.array((cols, rows)) / 2. + translation = np.floor(np.array((cols, rows )) / 2.) tform1 = SimilarityTransform(translation=-translation) tform2 = SimilarityTransform(rotation=np.deg2rad(angle)) tform3 = SimilarityTransform(translation=translation) @@ -257,16 +257,19 @@ def rotate(image, angle, preserve_shape=False, order=1, output_shape = None if not preserve_shape: # determine shape of output image - corners = tform([[0, 0], [0, rows], [cols, 0], [cols, rows]]) - corners = np.round(corners, 4) - minc = np.floor(corners[:, 0].min()) - minr = np.floor(corners[:, 1].min()) - maxc = np.ceil(corners[:, 0].max()) - maxr = np.ceil(corners[:, 1].max()) - output_shape = [maxr - minr, maxc - minc] + corners = np.array([[1, 1], [1, rows], [cols, 1], [cols, rows]]) + corners = tform2(tform1(corners - 1)) + minc = corners[:, 0].min() + minr = corners[:, 1].min() + maxc = corners[:, 0].max() + maxr = corners[:, 1].max() + out_rows = maxr - minr + 1 + out_cols = maxc - minc + 1 + output_shape = (out_rows, out_cols) # fit output image in new shape - tform4 = SimilarityTransform(translation=(minc, minr + 1)) + translation = ((cols - out_cols) / 2., (rows - out_rows) / 2.) + tform4 = SimilarityTransform(translation=translation) tform = tform4 + tform return warp(image, tform, output_shape=output_shape, order=order, From c0538c03e90cda78670855590618eb113ab23824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 27 Aug 2012 13:21:11 +0200 Subject: [PATCH 391/648] Fix translation bug in image rotate --- skimage/transform/_warps.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index e9cb61e5..14f0e187 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -248,7 +248,7 @@ def rotate(image, angle, preserve_shape=False, order=1, rows, cols = image.shape[0], image.shape[1] # rotation around center - translation = np.floor(np.array((cols, rows )) / 2.) + translation = np.array((cols, rows)) / 2. - 0.5 tform1 = SimilarityTransform(translation=-translation) tform2 = SimilarityTransform(rotation=np.deg2rad(angle)) tform3 = SimilarityTransform(translation=translation) @@ -257,15 +257,15 @@ def rotate(image, angle, preserve_shape=False, order=1, output_shape = None if not preserve_shape: # determine shape of output image - corners = np.array([[1, 1], [1, rows], [cols, 1], [cols, rows]]) - corners = tform2(tform1(corners - 1)) + corners = np.array([[1, 1], [1, rows], [cols, rows], [cols, 1]]) + corners = tform(corners - 1) minc = corners[:, 0].min() minr = corners[:, 1].min() maxc = corners[:, 0].max() maxr = corners[:, 1].max() out_rows = maxr - minr + 1 out_cols = maxc - minc + 1 - output_shape = (out_rows, out_cols) + output_shape = np.ceil((out_rows, out_cols)) # fit output image in new shape translation = ((cols - out_cols) / 2., (rows - out_rows) / 2.) From f4a3b44355e46be1c69feae9a57a1e0899ef8244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 27 Aug 2012 13:23:43 +0200 Subject: [PATCH 392/648] Rename parameter of image rotation --- skimage/transform/_warps.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 14f0e187..2a2d7828 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -211,8 +211,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, return clipped.squeeze() -def rotate(image, angle, preserve_shape=False, order=1, - mode='constant', cval=0.): +def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): """Rotate image by a certain angle around its center. Parameters @@ -221,7 +220,7 @@ def rotate(image, angle, preserve_shape=False, order=1, Input image. angle : float Rotation angle in degrees in counter-clockwise direction. - preserve_shape : bool, optional + resize: bool, optional Determine whether the shape of the output image will be automatically calculated, so the complete rotated image exactly fits. Default is False. @@ -255,7 +254,7 @@ def rotate(image, angle, preserve_shape=False, order=1, tform = tform1 + tform2 + tform3 output_shape = None - if not preserve_shape: + if not resize: # determine shape of output image corners = np.array([[1, 1], [1, rows], [cols, rows], [cols, 1]]) corners = tform(corners - 1) From 9a46111e761e527d337a628f1c263dfebe89cb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 27 Aug 2012 13:35:36 +0200 Subject: [PATCH 393/648] Apply numpy doc style for deprecation warning --- skimage/transform/_warps.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 2a2d7828..c6e22a19 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -349,8 +349,11 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, def homography(image, H, output_shape=None, order=1, mode='constant', cval=0.): """ - .. deprecated:: - 0.7 + .. note:: Deprecated in skimage 0.7 + `homography` will be removed in skimage 0.8, it is replaced by + `warp` because the latter provides the same functionality:: + + warp(image, ProjectiveTransform(H)) Perform a projective transformation (homography) on an image. From bcf45941700e3d1f7bde20c2070f237b45fcd1a3 Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Mon, 27 Aug 2012 15:18:03 +0200 Subject: [PATCH 394/648] use existing functionality for fix --- skimage/filter/_tv_denoise.py | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/skimage/filter/_tv_denoise.py b/skimage/filter/_tv_denoise.py index af8decfc..545f9e09 100644 --- a/skimage/filter/_tv_denoise.py +++ b/skimage/filter/_tv_denoise.py @@ -1,4 +1,5 @@ import numpy as np +from skimage.util import dtype def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): @@ -169,30 +170,6 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): E_previous = E i += 1 return out - -def _renormalize_image(image, data_type): - """ - inplace renormalize image date to obey the float range [0.0:1.0] - - Parameters - ---------- - immage: ndarray - input data to be renormalized - - data_type: type, - image data type before running tv_denoise - - Notes - ------- - the minimum and maximum values of the image data type define the range - which is mapped to the interval [0.0:1.0] - - """ - type_info = np.iinfo(data_type) - start = type_info.min - width = type_info.max - type_info.min - np.subtract(image, start, image) - np.divide(image, width, image) def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): """ @@ -281,5 +258,5 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): return out.astype(im_type) else: if not im_type.kind == 'f': - _renormalize_image(out, im_type) + out = dtype.convert(out.astype(im_type), np.float) return out From 06065f640a0e4ff895c884102e8a2345b5fcd7fd Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 27 Aug 2012 16:23:11 +0200 Subject: [PATCH 395/648] PEP 8: _quickshift module --- skimage/segmentation/_quickshift.pyx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/skimage/segmentation/_quickshift.pyx b/skimage/segmentation/_quickshift.pyx index 57be1040..b465eb08 100644 --- a/skimage/segmentation/_quickshift.pyx +++ b/skimage/segmentation/_quickshift.pyx @@ -13,8 +13,8 @@ from ..color import rgb2lab @cython.boundscheck(False) @cython.wraparound(False) @cython.cdivision(True) -def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=False, - sigma=0, convert2lab=True, random_seed=None): +def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, + return_tree=False, sigma=0, convert2lab=True, random_seed=None): """Segments image using quickshift clustering in Color-(x,y) space. Produces an oversegmentation of the image using the quickshift mode-seeking @@ -106,7 +106,8 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=Fa for c_ in range(c_min, c_max): dist = 0 for channel in range(channels): - dist += (current_pixel_p[channel] - image_c[r_, c_, channel])**2 + dist += (current_pixel_p[channel] - + image_c[r_, c_, channel])**2 dist += (r - r_)**2 + (c - c_)**2 densities[r, c] += exp(-dist / (2 * kernel_size**2)) current_pixel_p += channels @@ -132,9 +133,11 @@ def quickshift(image, ratio=1., float kernel_size=5, max_dist=10, return_tree=Fa if densities[r_, c_] > current_density: dist = 0 # We compute the distances twice since otherwise - # we get crazy memory overhead (width * height * windowsize**2) + # we get crazy memory overhead + # (width * height * windowsize**2) for channel in range(channels): - dist += (current_pixel_p[channel] - image_c[r_, c_, channel])**2 + dist += (current_pixel_p[channel] - + image_c[r_, c_, channel])**2 dist += (r - r_)**2 + (c - c_)**2 if dist < closest: closest = dist From d5710c82ab09cec222fc8b3361634bc08ed2e41e Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 27 Aug 2012 17:04:25 +0200 Subject: [PATCH 396/648] Example in route_through_array --- skimage/graph/mcp.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/skimage/graph/mcp.py b/skimage/graph/mcp.py index 68921371..941d6e89 100644 --- a/skimage/graph/mcp.py +++ b/skimage/graph/mcp.py @@ -1,7 +1,8 @@ from ._mcp import MCP, MCP_Geometric, make_offsets -def route_through_array(array, start, end, fully_connected=True, geometric=True): +def route_through_array(array, start, end, fully_connected=True, + geometric=True): """Simple example of how to use the MCP and MCP_Geometric classes. See the MCP and MCP_Geometric class documentation for explanation of the @@ -28,6 +29,44 @@ def route_through_array(array, start, end, fully_connected=True, geometric=True) List of n-d index tuples defining the path from `start` to `end`. cost : float Cost of the path. + + Examples + -------- + >>> from skimage.graph import route_through_array + >>> image = np.arange((36)).reshape((6, 6)) + >>> image + array([[ 0, 1, 2, 3, 4, 5], + [ 6, 7, 8, 9, 10, 11], + [12, 13, 14, 15, 16, 17], + [18, 19, 20, 21, 22, 23], + [24, 25, 26, 27, 28, 29], + [30, 31, 32, 33, 34, 35]]) + >>> # Find the path with lowest cost + >>> indices, weight = route_through_array(image, (0, 0), (5, 5)) + >>> indices = np.array(indices).T + >>> path = np.zeros_like(image) + >>> path[indices[0], indices[1]] = 1 + >>> path + array([[1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1]]) + >>> # Forbid diagonal steps + >>> indices, weight = route_through_array(image, (0, 0), (5, 5), \ + fully_connected=False) + >>> indices = np.array(indices).T + >>> path = np.zeros_like(image) + >>> path[indices[0], indices[1]] = 1 + >>> path + array([[1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1]]) + """ start, end = tuple(start), tuple(end) if geometric: From 6b86d4beeed84e84f62292c5a81a7f4cef71481b Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 27 Aug 2012 17:47:34 +0200 Subject: [PATCH 397/648] DOC: docstring of route_through_array --- skimage/graph/mcp.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/skimage/graph/mcp.py b/skimage/graph/mcp.py index 941d6e89..28843cbf 100644 --- a/skimage/graph/mcp.py +++ b/skimage/graph/mcp.py @@ -28,11 +28,33 @@ def route_through_array(array, start, end, fully_connected=True, path : list List of n-d index tuples defining the path from `start` to `end`. cost : float - Cost of the path. + Cost of the path. If `geometric` is False, the cost of the path is + the sum of the values of `array` along the path. If `geometric` is + True, a finer computation is made (see the documentation of the + MCP_Geometric class). + + See Also + -------- + MCP, MCP_Geometric Examples -------- >>> from skimage.graph import route_through_array + >>> image = np.array([[1, 3], [10, 12]]) + >>> image + array([[ 1, 3], + [10, 12]]) + >>> # Forbid diagonal steps + >>> route_through_array(image, [0, 0], [1, 1], fully_connected=False) + ([(0, 0), (0, 1), (1, 1)], 9.5) + >>> # Now allow diagonal steps: the path goes directly from start to end + >>> route_through_array(image, [0, 0], [1, 1]) + ([(0, 0), (1, 1)], 9.1923881554251192) + >>> # Cost is the sum of array values along the path (16 = 1 + 3 + 12) + >>> route_through_array(image, [0, 0], [1, 1], fully_connected=False, + ... geometric=False) + ([(0, 0), (0, 1), (1, 1)], 16.0) + >>> # Larger array where we display the path that is selected >>> image = np.arange((36)).reshape((6, 6)) >>> image array([[ 0, 1, 2, 3, 4, 5], @@ -53,19 +75,6 @@ def route_through_array(array, start, end, fully_connected=True, [0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 1]]) - >>> # Forbid diagonal steps - >>> indices, weight = route_through_array(image, (0, 0), (5, 5), \ - fully_connected=False) - >>> indices = np.array(indices).T - >>> path = np.zeros_like(image) - >>> path[indices[0], indices[1]] = 1 - >>> path - array([[1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 1]]) """ start, end = tuple(start), tuple(end) From 501c401ddad3b2614abd99d3513a8c19fea25eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 27 Aug 2012 18:05:18 +0200 Subject: [PATCH 398/648] Move image transform functions to _geometric file --- skimage/transform/__init__.py | 4 +- skimage/transform/_geometric.py | 206 +++++++++++++++++++++ skimage/transform/_warps.py | 211 +--------------------- skimage/transform/tests/test_geometric.py | 9 +- 4 files changed, 213 insertions(+), 217 deletions(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 1ee519f2..511c8738 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -2,7 +2,7 @@ from .hough_transform import * from .radon_transform import * from .finite_radon_transform import * from .integral import * -from ._geometric import (estimate_transform, +from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform) -from ._warps import warp, warp_coords, rotate, swirl, homography +from ._warps import rotate, swirl, homography diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index e4799375..f11f1b44 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1,5 +1,8 @@ import math import numpy as np +from scipy import ndimage +from skimage.util import img_as_float +from ._warps_cy import _warp_fast class GeometricTransform(object): @@ -584,6 +587,11 @@ TRANSFORMS = { 'projective': ProjectiveTransform, 'polynomial': PolynomialTransform, } +HOMOGRAPHY_TRANSFORMS = ( + SimilarityTransform, + AffineTransform, + ProjectiveTransform +) def estimate_transform(ttype, src, dst, **kwargs): @@ -671,3 +679,201 @@ def matrix_transform(coords, matrix): """ return ProjectiveTransform(matrix)(coords) + + +def _stackcopy(a, b): + """Copy b into each color layer of a, such that:: + + a[:,:,0] = a[:,:,1] = ... = b + + Parameters + ---------- + a : (M, N) or (M, N, P) ndarray + Target array. + b : (M, N) + Source array. + + Notes + ----- + Color images are stored as an ``(M, N, 3)`` or ``(M, N, 4)`` arrays. + + """ + if a.ndim == 3: + a[:] = b[:, :, np.newaxis] + else: + a[:] = b + + +def warp_coords(coord_map, shape, dtype=np.float64): + """Build the source coordinates for the output pixels of an image warp. + + Parameters + ---------- + coord_map : callable like GeometricTransform.inverse + Return input coordinates for given output coordinates. + shape : tuple + Shape of output image ``(rows, cols[, bands])``. + dtype : np.dtype or string + dtype for return value (sane choices: float32 or float64). + + Returns + ------- + coords : (ndim, rows, cols[, bands]) array of dtype `dtype` + Coordinates for `scipy.ndimage.map_coordinates`, that will yield + an image of shape (orows, ocols, bands) by drawing from source + points according to the `coord_transform_fn`. + + Notes + ----- + This is a lower-level routine that produces the source coordinates used by + `warp()`. + + It is provided separately from `warp` to give additional flexibility to + users who would like, for example, to re-use a particular coordinate + mapping, to use specific dtypes at various points along the the + image-warping process, or to implement different post-processing logic + than `warp` performs after the call to `ndimage.map_coordinates`. + + + Examples + -------- + Produce a coordinate map that Shifts an image to the right: + + >>> from skimage import data + >>> from scipy.ndimage import map_coordinates + >>> + >>> def shift_right(xy): + ... xy[:, 0] -= 10 + ... return xy + >>> + >>> image = data.lena().astype(np.float32) + >>> coords = warp_coords(shift_right, image.shape) + >>> warped_image = map_coordinates(image, coords) + + """ + rows, cols = shape[0], shape[1] + coords_shape = [len(shape), rows, cols] + if len(shape) == 3: + coords_shape.append(shape[2]) + coords = np.empty(coords_shape, dtype=dtype) + + # Reshape grid coordinates into a (P, 2) array of (x, y) pairs + tf_coords = np.indices((cols, rows), dtype=dtype).reshape(2, -1).T + + # Map each (x, y) pair to the source image according to + # the user-provided mapping + tf_coords = coord_map(tf_coords) + + # Reshape back to a (2, M, N) coordinate grid + tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2) + + # Place the y-coordinate mapping + _stackcopy(coords[1, ...], tf_coords[0, ...]) + + # Place the x-coordinate mapping + _stackcopy(coords[0, ...], tf_coords[1, ...]) + + if len(shape) == 3: + coords[2, ...] = range(shape[2]) + + return coords + + +def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, + mode='constant', cval=0., reverse_map=None): + """Warp an image according to a given coordinate transformation. + + Parameters + ---------- + image : 2-D array + Input image. + inverse_map : transformation object, callable xy = f(xy, **kwargs) + Inverse coordinate map. A function that transforms a (N, 2) array of + ``(x, y)`` coordinates in the *output image* into their corresponding + coordinates in the *source image* (e.g. a transformation object or its + inverse). + map_args : dict, optional + Keyword arguments passed to `inverse_map`. + output_shape : tuple (rows, cols) + Shape of the output image generated. + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : float + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Examples + -------- + Shift an image to the right: + + >>> from skimage import data + >>> image = data.camera() + >>> + >>> def shift_right(xy): + ... xy[:, 0] -= 10 + ... return xy + >>> + >>> warp(image, shift_right) + + """ + # Backward API compatibility + if reverse_map is not None: + inverse_map = reverse_map + + if image.ndim < 2: + raise ValueError("Input must have more than 1 dimension.") + + orig_ndim = image.ndim + image = np.atleast_3d(img_as_float(image)) + ishape = np.array(image.shape) + bands = ishape[2] + + # use fast Cython version for specific parameters + fast_modes = ('constant', 'reflect', 'wrap') + if order in (0, 1) and mode in fast_modes and not map_args: + matrix = None + if isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): + matrix = inverse_map._matrix + elif inverse_map.__name__ == 'inverse' \ + and inverse_map.im_class in HOMOGRAPHY_TRANSFORMS: + matrix = np.linalg.inv(inverse_map.im_self._matrix) + if matrix is not None: + # transform all bands + dims = [] + for dim in range(image.shape[2]): + dims.append(_warp_fast(image[..., dim], matrix, + output_shape=output_shape, + order=order, mode=mode, cval=cval)) + out = np.dstack(dims) + if orig_ndim == 2: + out = out[..., 0] + return out + + if output_shape is None: + output_shape = ishape + + rows, cols = output_shape[:2] + + def coord_map(*args): + return inverse_map(*args, **map_args) + + coords = warp_coords(coord_map, (rows, cols, bands)) + + # Prefilter not necessary for order 1 interpolation + prefilter = order > 1 + mapped = ndimage.map_coordinates(image, coords, prefilter=prefilter, + mode=mode, order=order, cval=cval) + + # The spline filters sometimes return results outside [0, 1], + # so clip to ensure valid data + clipped = np.clip(mapped, 0, 1) + + if mode == 'constant' and not (0 <= cval <= 1): + clipped[mapped == cval] = cval + + # Remove singleton dim introduced by atleast_3d + return clipped.squeeze() diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index c6e22a19..4e5a7245 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -1,214 +1,5 @@ import numpy as np -from scipy import ndimage -from skimage.util import img_as_float -from ._geometric import (SimilarityTransform, AffineTransform, - ProjectiveTransform) -from ._warps_cy import _warp_fast - - -HOMOGRAPHY_TRANSFORMS = ( - SimilarityTransform, - AffineTransform, - ProjectiveTransform -) - - -def _stackcopy(a, b): - """Copy b into each color layer of a, such that:: - - a[:,:,0] = a[:,:,1] = ... = b - - Parameters - ---------- - a : (M, N) or (M, N, P) ndarray - Target array. - b : (M, N) - Source array. - - Notes - ----- - Color images are stored as an ``(M, N, 3)`` or ``(M, N, 4)`` arrays. - - """ - if a.ndim == 3: - a[:] = b[:, :, np.newaxis] - else: - a[:] = b - - -def warp_coords(coord_map, shape, dtype=np.float64): - """Build the source coordinates for the output pixels of an image warp. - - Parameters - ---------- - coord_map : callable like GeometricTransform.inverse - Return input coordinates for given output coordinates. - shape : tuple - Shape of output image ``(rows, cols[, bands])``. - dtype : np.dtype or string - dtype for return value (sane choices: float32 or float64). - - Returns - ------- - coords : (ndim, rows, cols[, bands]) array of dtype `dtype` - Coordinates for `scipy.ndimage.map_coordinates`, that will yield - an image of shape (orows, ocols, bands) by drawing from source - points according to the `coord_transform_fn`. - - Notes - ----- - This is a lower-level routine that produces the source coordinates used by - `warp()`. - - It is provided separately from `warp` to give additional flexibility to - users who would like, for example, to re-use a particular coordinate - mapping, to use specific dtypes at various points along the the - image-warping process, or to implement different post-processing logic - than `warp` performs after the call to `ndimage.map_coordinates`. - - - Examples - -------- - Produce a coordinate map that Shifts an image to the right: - - >>> from skimage import data - >>> from scipy.ndimage import map_coordinates - >>> - >>> def shift_right(xy): - ... xy[:, 0] -= 10 - ... return xy - >>> - >>> image = data.lena().astype(np.float32) - >>> coords = warp_coords(shift_right, image.shape) - >>> warped_image = map_coordinates(image, coords) - - """ - rows, cols = shape[0], shape[1] - coords_shape = [len(shape), rows, cols] - if len(shape) == 3: - coords_shape.append(shape[2]) - coords = np.empty(coords_shape, dtype=dtype) - - # Reshape grid coordinates into a (P, 2) array of (x, y) pairs - tf_coords = np.indices((cols, rows), dtype=dtype).reshape(2, -1).T - - # Map each (x, y) pair to the source image according to - # the user-provided mapping - tf_coords = coord_map(tf_coords) - - # Reshape back to a (2, M, N) coordinate grid - tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2) - - # Place the y-coordinate mapping - _stackcopy(coords[1, ...], tf_coords[0, ...]) - - # Place the x-coordinate mapping - _stackcopy(coords[0, ...], tf_coords[1, ...]) - - if len(shape) == 3: - coords[2, ...] = range(shape[2]) - - return coords - - -def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, - mode='constant', cval=0., reverse_map=None): - """Warp an image according to a given coordinate transformation. - - Parameters - ---------- - image : 2-D array - Input image. - inverse_map : transformation object, callable xy = f(xy, **kwargs) - Inverse coordinate map. A function that transforms a (N, 2) array of - ``(x, y)`` coordinates in the *output image* into their corresponding - coordinates in the *source image* (e.g. a transformation object or its - inverse). - map_args : dict, optional - Keyword arguments passed to `inverse_map`. - output_shape : tuple (rows, cols) - Shape of the output image generated. - order : int - Order of splines used in interpolation. See - `scipy.ndimage.map_coordinates` for detail. - mode : string - How to handle values outside the image borders. See - `scipy.ndimage.map_coordinates` for detail. - cval : float - Used in conjunction with mode 'constant', the value outside - the image boundaries. - - Examples - -------- - Shift an image to the right: - - >>> from skimage import data - >>> image = data.camera() - >>> - >>> def shift_right(xy): - ... xy[:, 0] -= 10 - ... return xy - >>> - >>> warp(image, shift_right) - - """ - # Backward API compatibility - if reverse_map is not None: - inverse_map = reverse_map - - if image.ndim < 2: - raise ValueError("Input must have more than 1 dimension.") - - orig_ndim = image.ndim - image = np.atleast_3d(img_as_float(image)) - ishape = np.array(image.shape) - bands = ishape[2] - - # use fast Cython version for specific parameters - fast_modes = ('constant', 'reflect', 'wrap') - if order in (0, 1) and mode in fast_modes and not map_args: - matrix = None - if isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): - matrix = inverse_map._matrix - elif inverse_map.__name__ == 'inverse' \ - and inverse_map.im_class in HOMOGRAPHY_TRANSFORMS: - matrix = np.linalg.inv(inverse_map.im_self._matrix) - if matrix is not None: - # transform all bands - dims = [] - for dim in range(image.shape[2]): - dims.append(_warp_fast(image[..., dim], matrix, - output_shape=output_shape, - order=order, mode=mode, cval=cval)) - out = np.dstack(dims) - if orig_ndim == 2: - out = out[..., 0] - return out - - if output_shape is None: - output_shape = ishape - - rows, cols = output_shape[:2] - - def coord_map(*args): - return inverse_map(*args, **map_args) - - coords = warp_coords(coord_map, (rows, cols, bands)) - - # Prefilter not necessary for order 1 interpolation - prefilter = order > 1 - mapped = ndimage.map_coordinates(image, coords, prefilter=prefilter, - mode=mode, order=order, cval=cval) - - # The spline filters sometimes return results outside [0, 1], - # so clip to ensure valid data - clipped = np.clip(mapped, 0, 1) - - if mode == 'constant' and not (0 <= cval <= 1): - clipped[mapped == cval] = cval - - # Remove singleton dim introduced by atleast_3d - return clipped.squeeze() +from ._geometric import warp, SimilarityTransform def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index f035631d..57e95d46 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -1,10 +1,9 @@ import numpy as np from numpy.testing import assert_equal, assert_array_almost_equal - -from skimage.transform._warps import _stackcopy -from skimage.transform import (estimate_transform, SimilarityTransform, - AffineTransform, ProjectiveTransform, - PolynomialTransform) +from skimage.transform._geometric import _stackcopy +from skimage.transform import (estimate_transform, + SimilarityTransform, AffineTransform, + ProjectiveTransform, PolynomialTransform) SRC = np.array([ From 7bbb0aaa6337f1305a389b82ee613de4bab8ab24 Mon Sep 17 00:00:00 2001 From: Marianne Date: Mon, 27 Aug 2012 18:07:13 +0200 Subject: [PATCH 399/648] Fixed deprecated threshold. --- skimage/feature/_harris.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/_harris.py b/skimage/feature/_harris.py index 14854ac4..8ab40b22 100644 --- a/skimage/feature/_harris.py +++ b/skimage/feature/_harris.py @@ -105,5 +105,5 @@ def harris(image, min_distance=10, threshold=0.1, eps=1e-6, harrisim = _compute_harris_response(image, eps=eps, gaussian_deviation=gaussian_deviation) coordinates = peak.peak_local_max(harrisim, min_distance=min_distance, - threshold=threshold) + threshold_rel=threshold) return coordinates From 99e4264e15cd34d3875bd311470ce935a16c145b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 27 Aug 2012 18:15:39 +0200 Subject: [PATCH 400/648] Add missing return type to matrix transform function --- skimage/transform/_warps_cy.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index 68ccef27..2851a728 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -9,8 +9,8 @@ from skimage._shared.interpolation cimport (nearest_neighbour, bilinear_interpolation) -cdef inline _matrix_transform(double x, double y, double* H, double *x_, - double *y_): +cdef inline void _matrix_transform(double x, double y, double* H, double *x_, + double *y_): """Apply a homography to a coordinate. Parameters From f8e0dcfacab5112a4559a22c17881c31892ee1c8 Mon Sep 17 00:00:00 2001 From: Marianne Corvellec Date: Mon, 27 Aug 2012 18:18:47 +0200 Subject: [PATCH 401/648] Fixed layout (bracket alignment). --- skimage/feature/_harris.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/feature/_harris.py b/skimage/feature/_harris.py index 8ab40b22..ae30a29e 100644 --- a/skimage/feature/_harris.py +++ b/skimage/feature/_harris.py @@ -103,7 +103,7 @@ def harris(image, min_distance=10, threshold=0.1, eps=1e-6, """ harrisim = _compute_harris_response(image, eps=eps, - gaussian_deviation=gaussian_deviation) + gaussian_deviation=gaussian_deviation) coordinates = peak.peak_local_max(harrisim, min_distance=min_distance, - threshold_rel=threshold) + threshold_rel=threshold) return coordinates From 8955dad32ecc2caeda2c4448b20eaf0c07d0675a Mon Sep 17 00:00:00 2001 From: JDWarner Date: Mon, 27 Aug 2012 11:42:30 -0500 Subject: [PATCH 402/648] Multispectral modifications applied to random walker. --- .../random_walker_segmentation.py | 76 +++++++++++++------ 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 46f8dfb3..49490dda 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -64,17 +64,30 @@ def _make_graph_edges_3d(n_x, n_y, n_z): return edges -def _compute_weights_3d(data, beta=130, eps=1.e-6): - gradients = _compute_gradients_3d(data)**2 - beta /= 10 * data.std() +def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1.): + # Weight calculation is main difference in multispectral version + # Original gradient**2 replaced with sqrt( sum of gradients**2 ) + for i, spectrum in enumerate(data): + if i == 0: + gradients = _compute_gradients_3d(spectrum, depth=depth)**2 + else: + gradients += _compute_gradients_3d(spectrum)**2 + + gradients = np.sqrt(gradients) + + # New final term in beta to give == results in trivial case where + # multiple identical spectra are passed. + # It may be faster and/or more memory efficient do an approximate + # std() combining spectrum.std() together than this 2nd term. + beta /= 10 * np.asarray(data).std() * np.sqrt( len(data) ) gradients *= beta weights = np.exp(- gradients) weights += eps return weights -def _compute_gradients_3d(data): - gr_deep = np.abs(data[:, :, :-1] - data[:, :, 1:]).ravel() +def _compute_gradients_3d(data, depth=1.): + gr_deep = np.abs(data[:, :, :-1] - data[:, :, 1:]).ravel() / depth gr_right = np.abs(data[:, :-1] - data[:, 1:]).ravel() gr_down = np.abs(data[:-1] - data[1:]).ravel() return np.r_[gr_deep, gr_right, gr_down] @@ -148,10 +161,10 @@ def _mask_edges_weights(edges, weights, mask): return edges, weights -def _build_laplacian(data, mask=None, beta=50): - l_x, l_y, l_z = data.shape +def _build_laplacian(data, mask=None, beta=50, depth=1.): + l_x, l_y, l_z = data[0].shape edges = _make_graph_edges_3d(l_x, l_y, l_z) - weights = _compute_weights_3d(data, beta=beta, eps=1.e-10) + weights = _compute_weights_3d(data, beta=beta, eps=1.e-10, depth=depth) if mask is not None: edges, weights = _mask_edges_weights(edges, weights, mask) lap = _make_laplacian_sparse(edges, weights) @@ -162,17 +175,19 @@ def _build_laplacian(data, mask=None, beta=50): #----------- Random walker algorithm -------------------------------- -def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, - return_full_prob=False): +def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, + copy=True, return_full_prob=False): """ - Random walker algorithm for segmentation from markers. + Multispectral random walker algorithm for segmentation from markers. Parameters ---------- - data : array_like - Image to be segmented in phases. `data` can be two- or - three-dimensional. + data : array_like OR iterable of arrays + Image to be segmented in phases. Non-multispectral `data` can be + two- or three-dimensional; multispectral data is provided as an + iterable of like-sized 2D or 3D arrays. Data spacing is assumed + isotropic unless depth kwarg is used. labels : array of ints, of same shape as `data` Array of seed markers labeled with different positive integers @@ -186,6 +201,11 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, Penalization coefficient for the random walker motion (the greater `beta`, the more difficult the diffusion). + depth : float, default 1. + Correction for non-isotropic voxel depths in 3D volumes. + Default (1.) implies isotropy. This factor is derived as follows: + depth = (slice thickness) / (in-plane voxel dimension) + mode : {'bf', 'cg_mg', 'cg'} (default: 'bf') Mode for solving the linear system in the random walker algorithm. @@ -299,9 +319,19 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]]) """ - # We work with 3-D arrays of floats - data = img_as_float(data) - data = np.atleast_3d(data) + # Parse input data + if isinstance( data, np.ndarray ): + # Wrap into single-element list + data = [ np.atleast_3d( img_as_float(data) ) ] + else: + # We work with 3-D arrays of floats + newdata = [] + for spectrum in data: + newdata.append( np.atleast_3d( img_as_float(spectrum) ) ) + del data + data = newdata + del newdata + if copy: labels = np.copy(labels) label_values = np.unique(labels) @@ -318,9 +348,10 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, del filled labels = np.atleast_3d(labels) if np.any(labels < 0): - lap_sparse = _build_laplacian(data, mask=labels >= 0, beta=beta) + lap_sparse = _build_laplacian(data, mask=labels >= 0, beta=beta, + depth=depth) else: - lap_sparse = _build_laplacian(data, beta=beta) + lap_sparse = _build_laplacian(data, beta=beta, depth=depth) lap_sparse, B = _buildAB(lap_sparse, labels) # We solve the linear system # lap_sparse X = B @@ -343,18 +374,19 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, X = _solve_bf(lap_sparse, B, return_full_prob=return_full_prob) # Clean up results - data = np.squeeze(data) + for spectrum in data: + spectrum = spectrum.squeeze() if return_full_prob: labels = labels.astype(np.float) X = np.array([_clean_labels_ar(Xline, labels, - copy=True).reshape(data.shape) for Xline in X]) + copy=True).reshape(data[0].shape) for Xline in X]) for i in range(1, int(labels.max()) + 1): mask_i = np.squeeze(labels == i) X[i - 1, mask_i] = 1 X[np.setdiff1d(np.arange(0, labels.max(), dtype=np.int), [i - 1]), mask_i] = 0 else: - X = _clean_labels_ar(X + 1, labels).reshape(data.shape) + X = _clean_labels_ar(X + 1, labels).reshape(data[0].shape) return X From feca48cc49215d1813020859ddb81ad6c9096c33 Mon Sep 17 00:00:00 2001 From: JDWarner Date: Mon, 27 Aug 2012 11:48:32 -0500 Subject: [PATCH 403/648] Added return_full_prob kwarg to solve call if pyamg not present. --- skimage/segmentation/random_walker_segmentation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 49490dda..4eb86ff7 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -366,7 +366,8 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, """pyamg (http://code.google.com/p/pyamg/)) is needed to use this mode, but is not installed. The 'cg' mode will be used instead.""") - X = _solve_cg(lap_sparse, B, tol=tol) + X = _solve_cg(lap_sparse, B, tol=tol, + return_full_prob=return_full_prob) else: X = _solve_cg_mg(lap_sparse, B, tol=tol, return_full_prob=return_full_prob) From bb51f62f93f347487edb71e0fcc518e80cf1a840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 16 Aug 2012 22:35:04 +0200 Subject: [PATCH 404/648] Add function to clear border in binary images --- skimage/morphology/_clear_border.py | 36 +++++++++++++++++++ skimage/morphology/tests/test_clear_border.py | 32 +++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 skimage/morphology/_clear_border.py create mode 100644 skimage/morphology/tests/test_clear_border.py diff --git a/skimage/morphology/_clear_border.py b/skimage/morphology/_clear_border.py new file mode 100644 index 00000000..4185b0d9 --- /dev/null +++ b/skimage/morphology/_clear_border.py @@ -0,0 +1,36 @@ +import numpy as np +from skimage.measure import regionprops +from skimage.morphology import label + + +def clear_border(image, buffer_size=0, bgval=0): + """Clear objects connected to image border. + + The changes will be applied to the input image. + + Parameters + ---------- + image : (N, M) array + binary image + buffer_size : int, optional + define additional buffer around image border + bgval : float or int, optional + value for cleared objects + + Returns + ------- + image : (N, M) array + cleared binary image + """ + rows, cols = image.shape + for prop in regionprops(label(image), ['BoundingBox', 'Image']): + minr, minc, maxr, maxc = prop['BoundingBox'] + if ( + minr <= buffer_size + or minc <= buffer_size + or maxr >= rows - buffer_size + or maxc >= cols - buffer_size + ): + r, c = np.nonzero(prop['Image']) + image[minr + r, minc + c] = bgval + return image diff --git a/skimage/morphology/tests/test_clear_border.py b/skimage/morphology/tests/test_clear_border.py new file mode 100644 index 00000000..c4cdd4af --- /dev/null +++ b/skimage/morphology/tests/test_clear_border.py @@ -0,0 +1,32 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_equal +from skimage.morphology import clear_border + + +def test_possible_hull(): + image = np.array( + [[0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [1, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + + # test default case + result = clear_border(image.copy()) + ref = image.copy() + ref[2, 0] = 0 + ref[0, -2] = 0 + assert_array_equal(result, ref) + + # test buffer + result = clear_border(image.copy(), 1) + assert_array_equal(result, np.zeros(result.shape)) + + # test background value + result = clear_border(image.copy(), 1, 2) + assert_array_equal(result, 2 * image) + + +if __name__ == "__main__": + np.testing.run_module_suite() From 156b484bc2150cfe6089a1658cbff08a7682274a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 27 Aug 2012 18:49:14 +0200 Subject: [PATCH 405/648] Refactor clear_border for better performance --- skimage/morphology/_clear_border.py | 47 ++++++++++++------- skimage/morphology/tests/test_clear_border.py | 4 +- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/skimage/morphology/_clear_border.py b/skimage/morphology/_clear_border.py index 4185b0d9..47de7e49 100644 --- a/skimage/morphology/_clear_border.py +++ b/skimage/morphology/_clear_border.py @@ -1,6 +1,5 @@ import numpy as np -from skimage.measure import regionprops -from skimage.morphology import label +from scipy.ndimage import label def clear_border(image, buffer_size=0, bgval=0): @@ -11,26 +10,42 @@ def clear_border(image, buffer_size=0, bgval=0): Parameters ---------- image : (N, M) array - binary image + Binary image. buffer_size : int, optional - define additional buffer around image border + Define additional buffer around image border. bgval : float or int, optional - value for cleared objects + Value for cleared objects. Returns ------- image : (N, M) array - cleared binary image + Cleared binary image. + """ + rows, cols = image.shape - for prop in regionprops(label(image), ['BoundingBox', 'Image']): - minr, minc, maxr, maxc = prop['BoundingBox'] - if ( - minr <= buffer_size - or minc <= buffer_size - or maxr >= rows - buffer_size - or maxc >= cols - buffer_size - ): - r, c = np.nonzero(prop['Image']) - image[minr + r, minc + c] = bgval + if buffer_size >= rows or buffer_size >= cols: + raise ValueError("buffer size may not be greater than image size") + + # create borders with buffer_size + borders = np.zeros_like(image, np.bool_) + ext = buffer_size + 1 + borders[:ext] = True + borders[- ext:] = True + borders[:, :ext] = True + borders[:, - ext:] = True + + labels, number = label(image) + + # determine all objects that are connected to borders + borders_indices = np.unique(labels[borders]) + indices = np.arange(number + 1) + # mask all label indices that are connected to borders + label_mask = np.in1d(indices, borders_indices) + # create mask for pixels to clear + mask = label_mask[labels.ravel()].reshape(labels.shape) + + # clear border pixels + image[mask] = bgval + return image diff --git a/skimage/morphology/tests/test_clear_border.py b/skimage/morphology/tests/test_clear_border.py index c4cdd4af..6d1cf562 100644 --- a/skimage/morphology/tests/test_clear_border.py +++ b/skimage/morphology/tests/test_clear_border.py @@ -3,7 +3,7 @@ from numpy.testing import assert_array_equal, assert_equal from skimage.morphology import clear_border -def test_possible_hull(): +def test_clear_border(): image = np.array( [[0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 1, 0, 0, 0, 0], @@ -25,7 +25,7 @@ def test_possible_hull(): # test background value result = clear_border(image.copy(), 1, 2) - assert_array_equal(result, 2 * image) + assert_array_equal(result, 2 * np.ones_like(image)) if __name__ == "__main__": From 1177cf1393e3ea0f4aee4a8ef14c5a577591075c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 27 Aug 2012 18:51:57 +0200 Subject: [PATCH 406/648] Move clear_border to segmentation package --- skimage/segmentation/__init__.py | 1 + skimage/{morphology => segmentation}/_clear_border.py | 0 skimage/{morphology => segmentation}/tests/test_clear_border.py | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) rename skimage/{morphology => segmentation}/_clear_border.py (100%) rename skimage/{morphology => segmentation}/tests/test_clear_border.py (94%) diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 69a580c6..9dca2bc3 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -3,3 +3,4 @@ from ._felzenszwalb import felzenszwalb from ._slic import slic from ._quickshift import quickshift from .boundaries import find_boundaries, visualize_boundaries +from ._clear_border import clear_border diff --git a/skimage/morphology/_clear_border.py b/skimage/segmentation/_clear_border.py similarity index 100% rename from skimage/morphology/_clear_border.py rename to skimage/segmentation/_clear_border.py diff --git a/skimage/morphology/tests/test_clear_border.py b/skimage/segmentation/tests/test_clear_border.py similarity index 94% rename from skimage/morphology/tests/test_clear_border.py rename to skimage/segmentation/tests/test_clear_border.py index 6d1cf562..d87f3d25 100644 --- a/skimage/morphology/tests/test_clear_border.py +++ b/skimage/segmentation/tests/test_clear_border.py @@ -1,6 +1,6 @@ import numpy as np from numpy.testing import assert_array_equal, assert_equal -from skimage.morphology import clear_border +from skimage.segmentation import clear_border def test_clear_border(): From 42ae537a69247ffba90f06270bbbc48176293fc4 Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Mon, 27 Aug 2012 19:44:30 +0200 Subject: [PATCH 407/648] convert image to float before performing tv_denoise operation now removed keep_type argument from tv_denoise which becomes obsolete with the previos change adapted tests --- skimage/filter/_tv_denoise.py | 18 ++++-------------- skimage/filter/tests/test_tv_denoise.py | 14 +++++++------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/skimage/filter/_tv_denoise.py b/skimage/filter/_tv_denoise.py index 545f9e09..76affb98 100644 --- a/skimage/filter/_tv_denoise.py +++ b/skimage/filter/_tv_denoise.py @@ -1,5 +1,5 @@ import numpy as np -from skimage.util import dtype +from skimage import img_as_float def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): @@ -171,7 +171,7 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): i += 1 return out -def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): +def tv_denoise(im, weight=50, eps=2.e-4, n_iter_max=200): """ Perform total-variation denoising on 2-d and 3-d images @@ -192,11 +192,6 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): (E_(n-1) - E_n) < eps * E_0 - keep_type: bool, optional (False) - whether the output has the same dtype as the input array. - keep_type is False by default, and the dtype of the output - is np.float - n_iter_max: int, optional maximal number of iterations used for the optimization. @@ -245,7 +240,7 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): """ im_type = im.dtype if not im_type.kind == 'f': - im = im.astype(np.float) + im = img_as_float(im) if im.ndim == 2: out = _tv_denoise_2d(im, weight, eps, n_iter_max) @@ -254,9 +249,4 @@ def tv_denoise(im, weight=50, eps=2.e-4, keep_type=False, n_iter_max=200): else: raise ValueError('only 2-d and 3-d images may be denoised with this ' 'function') - if keep_type: - return out.astype(im_type) - else: - if not im_type.kind == 'f': - out = dtype.convert(out.astype(im_type), np.float) - return out + return out diff --git a/skimage/filter/tests/test_tv_denoise.py b/skimage/filter/tests/test_tv_denoise.py index 47f53251..afee6b02 100644 --- a/skimage/filter/tests/test_tv_denoise.py +++ b/skimage/filter/tests/test_tv_denoise.py @@ -2,7 +2,7 @@ import numpy as np from numpy.testing import run_module_suite from skimage import filter, data, color -from skimage import img_as_uint +from skimage import img_as_uint, img_as_ubyte class TestTvDenoise(): @@ -29,8 +29,8 @@ class TestTvDenoise(): # test if the total variation has decreased assert (np.sqrt((grad_denoised**2).sum()) < np.sqrt((grad**2).sum()) / 2) - denoised_lena_int = filter.tv_denoise(img_as_uint(lena), - weight=60.0, keep_type=True) + denoised_lena_int = img_as_uint(filter.tv_denoise(img_as_ubyte(lena), + weight=60.0)) assert denoised_lena_int.dtype is np.dtype('uint16') def test_tv_denoise_float_result_range(self): @@ -55,13 +55,13 @@ class TestTvDenoise(): mask += 20 * np.random.randn(*mask.shape) mask[mask < 0] = 0 mask[mask > 255] = 255 - res = filter.tv_denoise(mask.astype(np.uint8), - weight=100, keep_type=True) + res = img_as_ubyte(filter.tv_denoise(mask.astype(np.uint8), + weight=100)) assert res.std() < mask.std() assert res.dtype is np.dtype('uint8') - res = filter.tv_denoise(mask.astype(np.uint8), weight=100) + res = img_as_ubyte(filter.tv_denoise(mask.astype(np.uint8), weight=100)) assert res.std() < mask.std() - assert res.dtype is not np.dtype('uint8') + # test wrong number of dimensions a = np.random.random((8, 8, 8, 8)) try: From 682d0535cd31b5af65237adacf6d9fb5a12885fd Mon Sep 17 00:00:00 2001 From: JDWarner Date: Mon, 27 Aug 2012 13:41:41 -0500 Subject: [PATCH 408/648] Added multispectral random walker test. Since the multispectral path is equivalent except for gradient calcs, only one test case is needed. This test is modeled on the 3-D non-multispectral version. If deemed necessary, adding a 2-D case would be simple. --- skimage/segmentation/tests/test_random_walker.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/skimage/segmentation/tests/test_random_walker.py b/skimage/segmentation/tests/test_random_walker.py index 4e5a74c3..e0415498 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -138,6 +138,18 @@ def test_3d_inactive(): assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all() return data, labels, old_labels, after_labels + +def test_multispectral(): + n = 30 + lx, ly, lz = n, n, n + data, labels = make_3d_syntheticdata( lx, ly, lz ) + data = [data, data] # Result should be identical + multi_labels = random_walker(data, labels, mode='cg') + single_labels = random_walker(data[0], labels, mode='cg') + assert (multi_labels.reshape(data[0].shape)[13:17, 13:17, 13:17] == 2).all() + assert (single_labels.reshape(data[0].shape)[13:17, 13:17, 13:17] == 2).all() + return data, multi_labels, single_labels, labels + if __name__ == '__main__': from numpy import testing testing.run_module_suite() From 61320957eb1fb3c6c50094fb419bca497e47d73f Mon Sep 17 00:00:00 2001 From: JDWarner Date: Wed, 29 Aug 2012 16:33:56 -0500 Subject: [PATCH 409/648] Changes based on PR review recommendations: input format, scaling, and bugfix. In this new version, all instances of 'spectrum' have been replaced with 'channel'. The documentation also reflects this change, and the new multichannel kwarg used to indicate multichannel input is named appropriately. New boolean multichannel kwarg added, which controls if the input has multiple channels or not. Input 'data' is now array_like for both gray-level and multichannel. This kwarg is needed mainly because a 3-D array could be either 3 spatial dimensions or a set of different 2-D channels. New scaling kwarg added (may be removed in future), controlling if data scaling is applied to ALL channels or each channel individually, if multichannel=True. No effect for gray-level data. Removed np.sqrt(gradients) in _compute_weights_3d(), which was a bug. Tests now pass consistently. New method for maintaining shape from input to output, where dims = data.shape prior to np.atleast_3d(). A theoretical (70,100,1) array passed should now result in a (70,100,1) shaped output, for example. Updated and fixed multispectral test script to work with new version. TODO: Additional test(s) likely needed to cover code branches from new kwargs. --- .../random_walker_segmentation.py | 112 +++++++++++------- .../segmentation/tests/test_random_walker.py | 11 +- 2 files changed, 78 insertions(+), 45 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 4eb86ff7..239c4dd8 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -64,22 +64,29 @@ def _make_graph_edges_3d(n_x, n_y, n_z): return edges -def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1.): +def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1., + multichannel=False): # Weight calculation is main difference in multispectral version # Original gradient**2 replaced with sqrt( sum of gradients**2 ) - for i, spectrum in enumerate(data): - if i == 0: - gradients = _compute_gradients_3d(spectrum, depth=depth)**2 - else: - gradients += _compute_gradients_3d(spectrum)**2 + if not multichannel: + gradients = _compute_gradients_3d( data, depth=depth )**2 + else: + for channel in range(data.shape[-1]): + if channel == 0: + gradients = _compute_gradients_3d(data[..., channel], + depth=depth)**2 + else: + gradients += _compute_gradients_3d(data[..., channel], + depth=depth)**2 - gradients = np.sqrt(gradients) + # gradients = np.sqrt(gradients) - # New final term in beta to give == results in trivial case where - # multiple identical spectra are passed. - # It may be faster and/or more memory efficient do an approximate - # std() combining spectrum.std() together than this 2nd term. - beta /= 10 * np.asarray(data).std() * np.sqrt( len(data) ) + # All channels considered together in this standard deviation + beta /= 10 * data.std() + if multichannel: + # New final term in beta to give == results in trivial case where + # multiple identical spectra are passed. + beta /= np.sqrt( data.shape[-1] ) gradients *= beta weights = np.exp(- gradients) weights += eps @@ -161,10 +168,14 @@ def _mask_edges_weights(edges, weights, mask): return edges, weights -def _build_laplacian(data, mask=None, beta=50, depth=1.): - l_x, l_y, l_z = data[0].shape +def _build_laplacian(data, mask=None, beta=50, depth=1., multichannel=False): + if not multichannel: + l_x, l_y, l_z = data.shape + else: + l_x, l_y, l_z = data.shape[0], data.shape[1], data.shape[2] edges = _make_graph_edges_3d(l_x, l_y, l_z) - weights = _compute_weights_3d(data, beta=beta, eps=1.e-10, depth=depth) + weights = _compute_weights_3d(data, beta=beta, eps=1.e-10, depth=depth, + multichannel=multichannel) if mask is not None: edges, weights = _mask_edges_weights(edges, weights, mask) lap = _make_laplacian_sparse(edges, weights) @@ -175,19 +186,21 @@ def _build_laplacian(data, mask=None, beta=50, depth=1.): #----------- Random walker algorithm -------------------------------- -def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, - copy=True, return_full_prob=False): +def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, + copy=True, multichannel=False, scaling='all', + return_full_prob=False): """ - Multispectral random walker algorithm for segmentation from markers. + Multichannel random walker algorithm for segmentation from markers. Parameters ---------- - data : array_like OR iterable of arrays - Image to be segmented in phases. Non-multispectral `data` can be - two- or three-dimensional; multispectral data is provided as an - iterable of like-sized 2D or 3D arrays. Data spacing is assumed - isotropic unless depth kwarg is used. + data : array_like + Image to be segmented in phases. Gray-level`data` can be two- or + three-dimensional; multichannel data can be three- or four- + dimensional (requires multichannel=True) with the highest + dimension denoting channels. Data spacing is assumed isotropic + unless depth keyword argument is used. labels : array of ints, of same shape as `data` Array of seed markers labeled with different positive integers @@ -236,6 +249,21 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, the result of the segmentation. Use copy=False if you want to save on memory. + multichannel : bool, default False + If True, input data is parsed as multichannel data (see 'data' above + for proper input format in this case) + + scaling : string, default 'all' + Controls input scaling if multichannel=True (otherwise no effect). + + - 'all' (default): Data from all channels is combined when scaling + input data to the range [0,1] as type np.float64. Recommended + option for RGB(A) inputs. + + - 'separate': Each channel is scaled individually, separate from the + others, to the range [0,1]. Select this if the channels are very + different, for example if one were x-ray CT and another MRI data. + return_full_prob : bool, default False If True, the probability that a pixel belongs to each of the labels will be returned, instead of only the most likely label. @@ -320,17 +348,22 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, """ # Parse input data - if isinstance( data, np.ndarray ): - # Wrap into single-element list - data = [ np.atleast_3d( img_as_float(data) ) ] - else: + if not multichannel: # We work with 3-D arrays of floats - newdata = [] - for spectrum in data: - newdata.append( np.atleast_3d( img_as_float(spectrum) ) ) - del data - data = newdata - del newdata + dims = data.shape + data = np.atleast_3d( img_as_float(data) ) + else: + dims = data[..., 0].shape + data = np.atleast_3d( data ) # Should never be needed + if scaling.lower().strip() == 'all': + data = img_as_float( data ) + else: + newdata = np.zeros(data.shape, dtype=np.float64) + for channel in range( data.shape[-1] ): + newdata[..., channel] = img_as_float( data[..., channel] ) + del data + data = newdata + del newdata if copy: labels = np.copy(labels) @@ -349,9 +382,10 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, labels = np.atleast_3d(labels) if np.any(labels < 0): lap_sparse = _build_laplacian(data, mask=labels >= 0, beta=beta, - depth=depth) + depth=depth, multichannel=multichannel) else: - lap_sparse = _build_laplacian(data, beta=beta, depth=depth) + lap_sparse = _build_laplacian(data, beta=beta, depth=depth, + multichannel=multichannel) lap_sparse, B = _buildAB(lap_sparse, labels) # We solve the linear system # lap_sparse X = B @@ -366,7 +400,7 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, """pyamg (http://code.google.com/p/pyamg/)) is needed to use this mode, but is not installed. The 'cg' mode will be used instead.""") - X = _solve_cg(lap_sparse, B, tol=tol, + X = _solve_cg(lap_sparse, B, tol=tol, return_full_prob=return_full_prob) else: X = _solve_cg_mg(lap_sparse, B, tol=tol, @@ -375,19 +409,17 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, X = _solve_bf(lap_sparse, B, return_full_prob=return_full_prob) # Clean up results - for spectrum in data: - spectrum = spectrum.squeeze() if return_full_prob: labels = labels.astype(np.float) X = np.array([_clean_labels_ar(Xline, labels, - copy=True).reshape(data[0].shape) for Xline in X]) + copy=True).reshape(dims) for Xline in X]) for i in range(1, int(labels.max()) + 1): mask_i = np.squeeze(labels == i) X[i - 1, mask_i] = 1 X[np.setdiff1d(np.arange(0, labels.max(), dtype=np.int), [i - 1]), mask_i] = 0 else: - X = _clean_labels_ar(X + 1, labels).reshape(data[0].shape) + X = _clean_labels_ar(X + 1, labels).reshape(dims) return X diff --git a/skimage/segmentation/tests/test_random_walker.py b/skimage/segmentation/tests/test_random_walker.py index e0415498..74397dbe 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -143,11 +143,12 @@ def test_multispectral(): n = 30 lx, ly, lz = n, n, n data, labels = make_3d_syntheticdata( lx, ly, lz ) - data = [data, data] # Result should be identical - multi_labels = random_walker(data, labels, mode='cg') - single_labels = random_walker(data[0], labels, mode='cg') - assert (multi_labels.reshape(data[0].shape)[13:17, 13:17, 13:17] == 2).all() - assert (single_labels.reshape(data[0].shape)[13:17, 13:17, 13:17] == 2).all() + data.shape += (1,) + data = data.repeat(2, axis=3) # Result should be identical + multi_labels = random_walker(data, labels, mode='cg', multichannel=True) + single_labels = random_walker(data[:,:,:,0], labels, mode='cg') + assert (multi_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all() + assert (single_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all() return data, multi_labels, single_labels, labels if __name__ == '__main__': From 99238c44a594da3eff6666aee597ea69ec464c88 Mon Sep 17 00:00:00 2001 From: JDWarner Date: Wed, 29 Aug 2012 17:07:13 -0500 Subject: [PATCH 410/648] sqrt(gradients) line removed --- skimage/segmentation/random_walker_segmentation.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 239c4dd8..6165770d 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -78,9 +78,6 @@ def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1., else: gradients += _compute_gradients_3d(data[..., channel], depth=depth)**2 - - # gradients = np.sqrt(gradients) - # All channels considered together in this standard deviation beta /= 10 * data.std() if multichannel: From 9b44e24f8ed9d711b8127ad8097acb9a0855304d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 07:33:39 +0200 Subject: [PATCH 411/648] Use function pointer for different interpolation methods --- skimage/_shared/interpolation.pxd | 6 +++--- skimage/_shared/interpolation.pyx | 6 +++--- skimage/feature/_texture.pyx | 2 +- skimage/transform/_warps_cy.pyx | 15 +++++++++------ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/skimage/_shared/interpolation.pxd b/skimage/_shared/interpolation.pxd index c883d00d..3e8e74e9 100644 --- a/skimage/_shared/interpolation.pxd +++ b/skimage/_shared/interpolation.pxd @@ -1,13 +1,13 @@ cdef inline double nearest_neighbour(double* image, int rows, int cols, double r, double c, char mode, - double cval=*) + double cval) cdef inline double bilinear_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval=*) + double cval) cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, - char mode, double cval=*) + char mode, double cval) cdef inline int coord_map(int dim, int coord, char mode) diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx index 83722aba..63f5b70c 100644 --- a/skimage/_shared/interpolation.pyx +++ b/skimage/_shared/interpolation.pyx @@ -7,7 +7,7 @@ from libc.math cimport ceil, floor, round cdef inline double nearest_neighbour(double* image, int rows, int cols, double r, double c, char mode, - double cval=0): + double cval): """Nearest neighbour interpolation at a given position in the image. Parameters @@ -31,7 +31,7 @@ cdef inline double nearest_neighbour(double* image, int rows, int cols, cdef inline double bilinear_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval=0): + double cval): """Bilinear interpolation at a given position in the image. Parameters @@ -65,7 +65,7 @@ cdef inline double bilinear_interpolation(double* image, int rows, int cols, cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, - char mode, double cval=0): + char mode, double cval): """Get a pixel from the image, taking wrapping mode into consideration. Parameters diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 5fb53e90..70a446bb 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -132,7 +132,7 @@ def _local_binary_pattern(np.ndarray[double, ndim=2] image, for c in range(image.shape[1]): for i in range(P): texture[i] = bilinear_interpolation(image.data, - rows, cols, r + coords[i, 0], c + coords[i, 1], 'C') + rows, cols, r + coords[i, 0], c + coords[i, 1], 'C', 0) # signed / thresholded texture for i in range(P): if texture[i] - image[r, c] >= 0: diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index 2851a728..5903380e 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -111,14 +111,17 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, cdef int rows = img.shape[0] cdef int cols = img.shape[1] + cdef double (*interp_func)(double*, int, int, double, double, + char, double) + if order == 0: + interp_func = nearest_neighbour + elif order == 1: + interp_func = bilinear_interpolation + for tfr in range(out_r): for tfc in range(out_c): _matrix_transform(tfc, tfr, M.data, &c, &r) - if order == 0: - out[tfr, tfc] = nearest_neighbour(img.data, rows, - cols, r, c, mode_c, cval) - elif order == 1: - out[tfr, tfc] = bilinear_interpolation(img.data, rows, - cols, r, c, mode_c, cval) + out[tfr, tfc] = interp_func(img.data, rows, cols, r, c, + mode_c, cval) return out From 7a0e0b8f338e8b758a7ed869fd9be86e011b6a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 07:37:07 +0200 Subject: [PATCH 412/648] Simplify mode determination --- skimage/transform/_warps_cy.pyx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index 5903380e..18b1a5b0 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -87,13 +87,7 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, if mode not in ('constant', 'wrap', 'reflect'): raise ValueError("Invalid mode specified. Please use " "`constant`, `wrap` or `reflect`.") - cdef char mode_c - if mode == 'constant': - mode_c = ord('C') - elif mode == 'wrap': - mode_c = ord('W') - elif mode == 'reflect': - mode_c = ord('R') + cdef char mode_c = ord(mode[0].upper()) cdef int out_r, out_c if output_shape is None: From abe5dc3cecd901fa896df7b9b74b8c885e665131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 09:28:03 +0200 Subject: [PATCH 413/648] Add (bi-)cubic interpolation --- skimage/_shared/interpolation.pxd | 13 +++- skimage/_shared/interpolation.pyx | 101 ++++++++++++++++++++++++-- skimage/transform/_geometric.py | 2 +- skimage/transform/_warps_cy.pyx | 9 ++- skimage/transform/tests/test_warps.py | 2 +- 5 files changed, 113 insertions(+), 14 deletions(-) diff --git a/skimage/_shared/interpolation.pxd b/skimage/_shared/interpolation.pxd index 3e8e74e9..40df6524 100644 --- a/skimage/_shared/interpolation.pxd +++ b/skimage/_shared/interpolation.pxd @@ -1,12 +1,19 @@ -cdef inline double nearest_neighbour(double* image, int rows, int cols, - double r, double c, char mode, - double cval) +cdef inline double nearest_neighbour_interpolation(double* image, int rows, + int cols, double r, + double c, char mode, + double cval) cdef inline double bilinear_interpolation(double* image, int rows, int cols, double r, double c, char mode, double cval) +cdef inline double cubic_interpolation(double x, double[4] f) + +cdef inline double bicubic_interpolation(double* image, int rows, int cols, + double r, double c, char mode, + double cval) + cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, char mode, double cval) diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx index 63f5b70c..bcebd78d 100644 --- a/skimage/_shared/interpolation.pyx +++ b/skimage/_shared/interpolation.pyx @@ -5,16 +5,17 @@ from libc.math cimport ceil, floor, round -cdef inline double nearest_neighbour(double* image, int rows, int cols, - double r, double c, char mode, - double cval): +cdef inline double nearest_neighbour_interpolation(double* image, int rows, + int 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 + rows, cols : int Shape of image. r, c : int Position at which to interpolate. @@ -23,6 +24,11 @@ cdef inline double nearest_neighbour(double* image, int rows, int cols, cval : double Constant value to use for constant mode. + Returns + ------- + value : double + Interpolated value. + """ return get_pixel(image, rows, cols, round(r), round(c), @@ -38,7 +44,7 @@ cdef inline double bilinear_interpolation(double* image, int rows, int cols, ---------- image : double array Input image. - rows, cols: int + rows, cols : int Shape of image. r, c : int Position at which to interpolate. @@ -47,6 +53,11 @@ cdef inline double bilinear_interpolation(double* image, int rows, int cols, cval : double Constant value to use for constant mode. + Returns + ------- + value : double + Interpolated value. + """ cdef double dr, dc cdef int minr, minc, maxr, maxc @@ -64,6 +75,79 @@ cdef inline double bilinear_interpolation(double* image, int rows, int cols, return (1 - dr) * top + dr * bottom +cdef inline double cubic_interpolation(double x, double[4] f): + """Ccubic 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, int rows, int 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 : int + Position at which to interpolate. + mode : {'C', 'W', 'R'} + Wrapping mode. Constant, Wrap or Reflect. + cval : double + Constant value to use for constant mode. + + Returns + ------- + value : double + Interpolated value. + + """ + + cdef int r0 = r + cdef int c0 = c + if r < 0: + r0 -= 1 + if c < 0: + c0 -= 1 + # scale position to range [0, 1] + cdef double xr = (r - r0 + 1) / 3 + cdef double xc = (c - c0 + 1) / 3 + + cdef double fc[4], fr[4] + + cdef int pr, pc + + for pr in range(r0 - 1, r0 + 3): + + # do row-wise cubic interpolation + for pc in range(c0 - 1, c0 + 3): + fc[pc + 1 - c0] = get_pixel(image, rows, cols, pr, pc, mode, cval) + fr[pr + 1 - r0] = cubic_interpolation(xc, fc) + + # do cubic interpolation for interpolated values of each row + return cubic_interpolation(xr, fr) + + cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, char mode, double cval): """Get a pixel from the image, taking wrapping mode into consideration. @@ -72,7 +156,7 @@ cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, ---------- image : double array Input image. - rows, cols: int + rows, cols : int Shape of image. r, c : int Position at which to get the pixel. @@ -81,6 +165,11 @@ cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, 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): diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index f11f1b44..6f895fa7 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -834,7 +834,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # use fast Cython version for specific parameters fast_modes = ('constant', 'reflect', 'wrap') - if order in (0, 1) and mode in fast_modes and not map_args: + if order in (0, 1, 3) and mode in fast_modes and not map_args: matrix = None if isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): matrix = inverse_map._matrix diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index 18b1a5b0..37a577f7 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -5,8 +5,9 @@ cimport numpy as np import numpy as np -from skimage._shared.interpolation cimport (nearest_neighbour, - bilinear_interpolation) +from skimage._shared.interpolation cimport (nearest_neighbour_interpolation, + bilinear_interpolation, + bicubic_interpolation) cdef inline void _matrix_transform(double x, double y, double* H, double *x_, @@ -108,9 +109,11 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, cdef double (*interp_func)(double*, int, int, double, double, char, double) if order == 0: - interp_func = nearest_neighbour + interp_func = nearest_neighbour_interpolation elif order == 1: interp_func = bilinear_interpolation + elif order == 3: + interp_func = bicubic_interpolation for tfr in range(out_r): for tfc in range(out_c): diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index d5ffff59..41d1a46a 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -57,7 +57,7 @@ def test_fast_homography(): tform = ProjectiveTransform(H) coords = warp_coords(tform.inverse, (img.shape[0], img.shape[1])) - for order in range(2): + for order in range(4): for mode in ('constant', 'reflect', 'wrap'): p0 = map_coordinates(img, coords, mode=mode, order=order) p1 = warp(img, tform, mode=mode, order=order) From bbeaec6b3faebc0c3308423303228e6c6b8a7259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 09:36:28 +0200 Subject: [PATCH 414/648] Add examples to doc string of clear_border --- skimage/segmentation/_clear_border.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/skimage/segmentation/_clear_border.py b/skimage/segmentation/_clear_border.py index 47de7e49..733f7000 100644 --- a/skimage/segmentation/_clear_border.py +++ b/skimage/segmentation/_clear_border.py @@ -21,6 +21,24 @@ def clear_border(image, buffer_size=0, bgval=0): image : (N, M) array Cleared binary image. + Examples + -------- + >>> import numpy as np + >>> from skimage.segmentation import clear_border + >>> image = np.array([[0, 0, 0, 0, 0, 0, 0, 1, 0], + ... [0, 0, 0, 0, 1, 0, 0, 0, 0], + ... [1, 0, 0, 1, 0, 1, 0, 0, 0], + ... [0, 0, 1, 1, 1, 1, 1, 0, 0], + ... [0, 1, 1, 1, 1, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> clear_border(image) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + """ rows, cols = image.shape From dd45f15ced95ed43b889f464b4116bb8f4124d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 09:40:54 +0200 Subject: [PATCH 415/648] Use explicit keyword for dtype --- skimage/segmentation/_clear_border.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/_clear_border.py b/skimage/segmentation/_clear_border.py index 733f7000..266e17e2 100644 --- a/skimage/segmentation/_clear_border.py +++ b/skimage/segmentation/_clear_border.py @@ -46,7 +46,7 @@ def clear_border(image, buffer_size=0, bgval=0): raise ValueError("buffer size may not be greater than image size") # create borders with buffer_size - borders = np.zeros_like(image, np.bool_) + borders = np.zeros_like(image, dtype=np.bool_) ext = buffer_size + 1 borders[:ext] = True borders[- ext:] = True From 1592e47e661c0bea7a965af09a7e3e1b3b020d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 09:47:00 +0200 Subject: [PATCH 416/648] Apply clipping also to fast cython implementation --- skimage/transform/_geometric.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 6f895fa7..130cfc2d 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -834,7 +834,9 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # use fast Cython version for specific parameters fast_modes = ('constant', 'reflect', 'wrap') - if order in (0, 1, 3) and mode in fast_modes and not map_args: + fast_orders = (0, 1, 3) + + if order in fast_orders and mode in fast_modes and not map_args: matrix = None if isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): matrix = inverse_map._matrix @@ -851,29 +853,30 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, out = np.dstack(dims) if orig_ndim == 2: out = out[..., 0] - return out - if output_shape is None: - output_shape = ishape + else: # use ndimage.map_coordinates - rows, cols = output_shape[:2] + if output_shape is None: + output_shape = ishape - def coord_map(*args): - return inverse_map(*args, **map_args) + rows, cols = output_shape[:2] - coords = warp_coords(coord_map, (rows, cols, bands)) + def coord_map(*args): + return inverse_map(*args, **map_args) - # Prefilter not necessary for order 1 interpolation - prefilter = order > 1 - mapped = ndimage.map_coordinates(image, coords, prefilter=prefilter, - mode=mode, order=order, cval=cval) + coords = warp_coords(coord_map, (rows, cols, bands)) + + # Prefilter not necessary for order 1 interpolation + prefilter = order > 1 + out = ndimage.map_coordinates(image, coords, prefilter=prefilter, + mode=mode, order=order, cval=cval) # The spline filters sometimes return results outside [0, 1], # so clip to ensure valid data - clipped = np.clip(mapped, 0, 1) + clipped = np.clip(out, 0, 1) if mode == 'constant' and not (0 <= cval <= 1): - clipped[mapped == cval] = cval + clipped[out == cval] = cval # Remove singleton dim introduced by atleast_3d return clipped.squeeze() From cb870fd069c16685aef0605c9f2a53fda3d948e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 09:53:41 +0200 Subject: [PATCH 417/648] Fix example of estimate_transform --- skimage/transform/_geometric.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 130cfc2d..95af55d3 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -643,8 +643,8 @@ def estimate_transform(ttype, src, dst, **kwargs): >>> warp(image, inverse_map=tform.inverse) >>> # create transformation with explicit parameters - >>> tform2 = tf.SimilarityTransform() - >>> tform2.compose_implicit(scale=1.1, rotation=1, translation=(10, 20)) + >>> tform2 = tf.SimilarityTransform(scale=1.1, rotation=1, + ... translation=(10, 20)) >>> # unite transformations, applied in order from left to right >>> tform3 = tform + tform2 From 4dfdc7f74f317fcf57c2ea460ba2e0715bc35d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 11:57:05 +0200 Subject: [PATCH 418/648] Update doc string of _warp_fast for bicubic interpolation --- skimage/transform/_warps_cy.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index 37a577f7..99eb2500 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -72,6 +72,7 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, Order of interpolation:: * 0: Nearest-neighbour interpolation. * 1: Bilinear interpolation (default). + * 3: Bicubic interpolation. mode : {'constant', 'reflect', 'wrap'} How to handle values outside the image borders. cval : string From 4cd1f8798b8ff0a615894bb1ac8f4dad7e99b5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 12:08:21 +0200 Subject: [PATCH 419/648] Add nearest mode for positions outside image --- skimage/_shared/interpolation.pyx | 35 +++++++++++++++------------ skimage/transform/_geometric.py | 7 ++---- skimage/transform/_warps_cy.pyx | 4 +-- skimage/transform/tests/test_warps.py | 2 +- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx index bcebd78d..fc247530 100644 --- a/skimage/_shared/interpolation.pyx +++ b/skimage/_shared/interpolation.pyx @@ -19,8 +19,8 @@ cdef inline double nearest_neighbour_interpolation(double* image, int rows, Shape of image. r, c : int Position at which to interpolate. - mode : {'C', 'W', 'R'} - Wrapping mode. Constant, Wrap or Reflect. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. cval : double Constant value to use for constant mode. @@ -48,8 +48,8 @@ cdef inline double bilinear_interpolation(double* image, int rows, int cols, Shape of image. r, c : int Position at which to interpolate. - mode : {'C', 'W', 'R'} - Wrapping mode. Constant, Wrap or Reflect. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. cval : double Constant value to use for constant mode. @@ -111,8 +111,8 @@ cdef inline double bicubic_interpolation(double* image, int rows, int cols, Shape of image. r, c : int Position at which to interpolate. - mode : {'C', 'W', 'R'} - Wrapping mode. Constant, Wrap or Reflect. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. cval : double Constant value to use for constant mode. @@ -160,8 +160,8 @@ cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, Shape of image. r, c : int Position at which to get the pixel. - mode : {'C', 'W', 'R'} - Wrapping mode. Constant, Wrap or Reflect. + mode : {'C', 'W', 'R', 'N'} + Wrapping mode. Constant, Wrap, Reflect or Nearest. cval : double Constant value to use for constant mode. @@ -190,28 +190,33 @@ cdef inline int coord_map(int dim, int coord, char mode): Maximum coordinate. coord : int Coord provided by user. May be < 0 or > dim. - mode : {'W', 'R'} + 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): + if coord < 0: # How many times times does the coordinate wrap? - if ((-coord / dim) % 2 != 0): + if (-coord / dim) % 2 != 0: return dim - (-coord % dim) else: return (-coord % dim) - elif (coord > dim): - if ((coord / dim) % 2 != 0): + elif coord > dim: + if (coord / dim) % 2 != 0: return (dim - (coord % dim)) else: return (coord % dim) elif mode == 'W': # wrap - if (coord < 0): + if coord < 0: return (dim - (-coord % dim)) - elif (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/transform/_geometric.py b/skimage/transform/_geometric.py index 95af55d3..02b6c161 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -832,11 +832,8 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, ishape = np.array(image.shape) bands = ishape[2] - # use fast Cython version for specific parameters - fast_modes = ('constant', 'reflect', 'wrap') - fast_orders = (0, 1, 3) - - if order in fast_orders and mode in fast_modes and not map_args: + # use fast Cython version for specific interpolation orders + if order in (0, 1, 3) and not map_args: matrix = None if isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): matrix = inverse_map._matrix diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index 99eb2500..bedbfa61 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -86,9 +86,9 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] M = \ np.ascontiguousarray(H) - if mode not in ('constant', 'wrap', 'reflect'): + if mode not in ('constant', 'wrap', 'reflect', 'nearest'): raise ValueError("Invalid mode specified. Please use " - "`constant`, `wrap` or `reflect`.") + "`constant`, `nearest`, `wrap` or `reflect`.") cdef char mode_c = ord(mode[0].upper()) cdef int out_r, out_c diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 41d1a46a..65514073 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -58,7 +58,7 @@ def test_fast_homography(): coords = warp_coords(tform.inverse, (img.shape[0], img.shape[1])) for order in range(4): - for mode in ('constant', 'reflect', 'wrap'): + for mode in ('constant', 'reflect', 'wrap', 'nearest'): p0 = map_coordinates(img, coords, mode=mode, order=order) p1 = warp(img, tform, mode=mode, order=order) From 146d5a3f5b460de4673f1e63fef1a6b21b8b8b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 17:13:30 +0200 Subject: [PATCH 420/648] Remove duplicate subtraction --- skimage/_shared/interpolation.pyx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx index fc247530..ad8aae4b 100644 --- a/skimage/_shared/interpolation.pyx +++ b/skimage/_shared/interpolation.pyx @@ -123,28 +123,27 @@ cdef inline double bicubic_interpolation(double* image, int rows, int cols, """ - cdef int r0 = r - cdef int c0 = c + cdef int r0 = r - 1 + cdef int c0 = c - 1 if r < 0: r0 -= 1 if c < 0: c0 -= 1 # scale position to range [0, 1] - cdef double xr = (r - r0 + 1) / 3 - cdef double xc = (c - c0 + 1) / 3 + cdef double xr = (r - r0) / 3 + cdef double xc = (c - c0) / 3 cdef double fc[4], fr[4] cdef int pr, pc - for pr in range(r0 - 1, r0 + 3): + # row-wise cubic interpolation + for pr in range(r0, r0 + 4): + for pc in range(c0, c0 + 4): + fc[pc - c0] = get_pixel(image, rows, cols, pr, pc, mode, cval) + fr[pr - r0] = cubic_interpolation(xc, fc) - # do row-wise cubic interpolation - for pc in range(c0 - 1, c0 + 3): - fc[pc + 1 - c0] = get_pixel(image, rows, cols, pr, pc, mode, cval) - fr[pr + 1 - r0] = cubic_interpolation(xc, fc) - - # do cubic interpolation for interpolated values of each row + # cubic interpolation for interpolated values of each row return cubic_interpolation(xr, fr) From 15cc7f1779524acd232caf3ad878a268e7f122b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 18:14:42 +0200 Subject: [PATCH 421/648] Add biquadratic interpolation --- skimage/_shared/interpolation.pxd | 6 ++- skimage/_shared/interpolation.pyx | 74 ++++++++++++++++++++++++++++++- skimage/transform/_geometric.py | 2 +- skimage/transform/_warps_cy.pyx | 4 ++ 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/skimage/_shared/interpolation.pxd b/skimage/_shared/interpolation.pxd index 40df6524..ef880109 100644 --- a/skimage/_shared/interpolation.pxd +++ b/skimage/_shared/interpolation.pxd @@ -8,8 +8,12 @@ cdef inline double bilinear_interpolation(double* image, int rows, int cols, double r, double c, char mode, double cval) -cdef inline double cubic_interpolation(double x, double[4] f) +cdef inline double quadratic_interpolation(double x, double[3] f) +cdef inline double biquadratic_interpolation(double* image, int rows, int cols, + double r, double c, char mode, + double cval) +cdef inline double cubic_interpolation(double x, double[4] f) cdef inline double bicubic_interpolation(double* image, int rows, int cols, double r, double c, char mode, double cval) diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx index ad8aae4b..3150f41b 100644 --- a/skimage/_shared/interpolation.pyx +++ b/skimage/_shared/interpolation.pyx @@ -75,8 +75,80 @@ cdef inline double bilinear_interpolation(double* image, int rows, int cols, 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, int rows, int 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 : int + 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 int r0 = round(r) + cdef int 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 int pr, pc + + # row-wise cubic interpolation + for pr in range(r0, r0 + 3): + for pc in range(c0, c0 + 3): + fc[pc - c0] = get_pixel(image, rows, cols, pr, pc, mode, cval) + 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): - """Ccubic interpolation. + """Cubic interpolation. Parameters ---------- diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 02b6c161..270198aa 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -833,7 +833,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, bands = ishape[2] # use fast Cython version for specific interpolation orders - if order in (0, 1, 3) and not map_args: + if order in range(4) and not map_args: matrix = None if isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): matrix = inverse_map._matrix diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index bedbfa61..ce400ed6 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -7,6 +7,7 @@ cimport numpy as np import numpy as np from skimage._shared.interpolation cimport (nearest_neighbour_interpolation, bilinear_interpolation, + biquadratic_interpolation, bicubic_interpolation) @@ -72,6 +73,7 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, Order of interpolation:: * 0: Nearest-neighbour interpolation. * 1: Bilinear interpolation (default). + * 2: Biquadratic interpolation (default). * 3: Bicubic interpolation. mode : {'constant', 'reflect', 'wrap'} How to handle values outside the image borders. @@ -113,6 +115,8 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, interp_func = nearest_neighbour_interpolation elif order == 1: interp_func = bilinear_interpolation + elif order == 2: + interp_func = biquadratic_interpolation elif order == 3: interp_func = bicubic_interpolation From b2036aee5c81a8a7f7caa9d39711c7100c222da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 18:41:00 +0200 Subject: [PATCH 422/648] Add image resize function --- skimage/transform/__init__.py | 2 +- skimage/transform/_warps.py | 51 ++++++++++++++++++++++++++- skimage/transform/tests/test_warps.py | 11 +++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 511c8738..0907544b 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -5,4 +5,4 @@ from .integral import * from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform) -from ._warps import rotate, swirl, homography +from ._warps import resize, rotate, swirl, homography diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 4e5a7245..1ed0bed9 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -1,5 +1,54 @@ import numpy as np -from ._geometric import warp, SimilarityTransform +from ._geometric import warp, SimilarityTransform, AffineTransform + + +def resize(image, output_shape, order=1, mode='constant', cval=0.): + """Resize image. + + Parameters + ---------- + image : ndarray + Input image. + output_shape : tuple or ndarray + Size of the generated output image `(rows, cols)`. + + Returns + ------- + resized : ndarray + Resized version of the input. + + Other parameters + ---------------- + order : int + Order of splines used in interpolation. See + `scipy.ndimage.map_coordinates` for detail. + mode : string + How to handle values outside the image borders. See + `scipy.ndimage.map_coordinates` for detail. + cval : string + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + """ + + rows, cols = output_shape + orig_rows, orig_cols = image.shape[0], image.shape[1] + + rscale = float(orig_rows) / rows + cscale = float(orig_cols) / cols + + # 3 control points necessary to estimate exact AffineTransform + src_corners = np.array([[1, 1], [1, rows], [cols, rows]]) - 1 + dst_corners = np.zeros(src_corners.shape, dtype=np.double) + # take into account that 0th pixel is at position (0.5, 0.5) + dst_corners[:, 0] = cscale * (src_corners[:, 0] + 0.5) - 0.5 + dst_corners[:, 1] = rscale * (src_corners[:, 1] + 0.5) - 0.5 + + tform = AffineTransform() + tform.estimate(src_corners, dst_corners) + + return warp(image, tform, output_shape=output_shape, order=order, + mode=mode, cval=cval) def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 65514073..ac9272c6 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -2,7 +2,7 @@ from numpy.testing import assert_array_almost_equal, run_module_suite import numpy as np from scipy.ndimage import map_coordinates -from skimage.transform import (warp, warp_coords, rotate, +from skimage.transform import (warp, warp_coords, rotate, resize, AffineTransform, ProjectiveTransform, SimilarityTransform) @@ -81,6 +81,15 @@ def test_rotate(): assert_array_almost_equal(x90, np.rot90(x)) +def test_resize(): + x = np.zeros((5, 5), dtype=np.double) + x[1, 1] = 1 + resized = resize(x, (10, 10), order=0) + ref = np.zeros((10, 10)) + ref[2:4, 2:4] = 1 + assert_array_almost_equal(resized, ref) + + def test_swirl(): image = img_as_float(data.checkerboard()) From cdff128a43fbf40cd04c177e871936bdfa6126b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 21:38:33 +0200 Subject: [PATCH 423/648] Add new Coordinates property to regionprops --- skimage/measure/_regionprops.py | 31 ++++++++++++++--------- skimage/measure/tests/test_regionprops.py | 8 ++++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index be10d6ec..dfc88b4f 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -77,8 +77,10 @@ def regionprops(label_image, properties=['Area', 'Centroid'], Centroid coordinate tuple `(row, col)`. * ConvexArea : int Number of pixels of convex hull image. - * ConvexImage : H x J ndarray + * ConvexImage : (H, J) ndarray Binary convex hull image which has the same size as bounding box. + * Coordinates : (N, 2) ndarray + Coordinate list `(row, col)` of the region. * Eccentricity : float Eccentricity of the ellipse that has the same second-moments as the region. The eccentricity is the ratio of the distance between its @@ -93,12 +95,12 @@ def regionprops(label_image, properties=['Area', 'Centroid'], Computed as `Area / (rows*cols)` * FilledArea : int Number of pixels of filled region. - * FilledImage : H x J ndarray + * FilledImage : (H, J) ndarray Binary region image with filled holes which has the same size as bounding box. * HuMoments : tuple Hu moments (translation, scale and rotation invariant). - * Image : H x J ndarray + * Image : (H, J) ndarray Sliced binary region image which has the same size as bounding box. * MajorAxisLength : float The length of the major axis of the ellipse that has the same @@ -142,7 +144,7 @@ def regionprops(label_image, properties=['Area', 'Centroid'], * WeightedHuMoments : tuple Hu moments (translation, scale and rotation invariant) of intensity image. - * WeightedMoments : 3 x 3 ndarray + * WeightedMoments : (3, 3) ndarray Spatial moments of intensity image up to 3rd order. wm_ji = sum{ array(x, y) * x^j * y^i } where the sum is over the `x`, `y` coordinates of the region. @@ -152,7 +154,7 @@ def regionprops(label_image, properties=['Area', 'Centroid'], wnu_ji = wmu_ji / wm_00^[(i+j)/2 + 1] where `wm_00` is the zeroth spatial moment (intensity-weighted area). - intensity_image : N x M ndarray, optional + intensity_image : (N, M) ndarray, optional Intensity image with same size as labelled image. Default is None. Returns @@ -163,13 +165,14 @@ def regionprops(label_image, properties=['Area', 'Centroid'], References ---------- - Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: Core - Algorithms. Springer-Verlag, London, 2009. - B. Jähne. Digital Image Processing. Springer-Verlag, - Berlin-Heidelberg, 6. edition, 2005. - T. H. Reiss. Recognizing Planar Objects Using Invariant Image Features, - from Lecture notes in computer science, p. 676. Springer, Berlin, 1993. - http://en.wikipedia.org/wiki/Image_moment + .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [4] http://en.wikipedia.org/wiki/Image_moment Examples -------- @@ -246,6 +249,10 @@ def regionprops(label_image, properties=['Area', 'Centroid'], _convex_image = convex_hull_image(array) obj_props['ConvexImage'] = _convex_image + if 'Coordinates' in properties: + rr, cc = np.nonzero(array) + obj_props['Coordinates'] = np.vstack((rr + r0, cc + c0)).T + if 'Eccentricity' in properties: if l1 == 0: obj_props['Eccentricity'] = 0 diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index 573c852a..271c1ec4 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -78,6 +78,14 @@ def test_convex_image(): assert_array_equal(img, ref) +def test_coordinates(): + sample = np.zeros((10, 10), dtype=np.int8) + coords = np.array([[3, 2], [3, 3], [3, 4]]) + sample[coords[:, 0], coords[:, 1]] = 1 + prop_coords = regionprops(sample, ['Coordinates'])[0]['Coordinates'] + assert_array_equal(prop_coords, coords) + + def test_eccentricity(): eps = regionprops(SAMPLE, ['Eccentricity'])[0]['Eccentricity'] assert_almost_equal(eps, 0.814629313427) From da3f2b5f4d6993130299bd54bf8f56e0004861b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 30 Aug 2012 22:00:43 +0200 Subject: [PATCH 424/648] Add example script for image labelling --- doc/examples/plot_label.py | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 doc/examples/plot_label.py diff --git a/doc/examples/plot_label.py b/doc/examples/plot_label.py new file mode 100644 index 00000000..7b2fd717 --- /dev/null +++ b/doc/examples/plot_label.py @@ -0,0 +1,49 @@ +""" +=================== +Label image regions +=================== + +This example shows how to segment an image with image labelling. + +""" + +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches + +from skimage import data +from skimage.filter import threshold_otsu +from skimage.segmentation import clear_border +from skimage.morphology import label +from skimage.measure import regionprops + + +image = data.coins()[50:-50, 50:-50] + +# apply threshold +thresh = threshold_otsu(image) +bw = image > thresh + +fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) +plt.gray() +ax.imshow(bw) + +# remove artifacts connected to image border +cleared = bw.copy() +clear_border(cleared) + +# label image regions +label_image = label(cleared) + +for region in regionprops(label_image, ['Area', 'BoundingBox']): + + # skip small images + if region['Area'] < 100: + continue + + # draw rectangle around segmented coins + minr, minc, maxr, maxc = region['BoundingBox'] + rect = mpatches.Rectangle((minc, minr), maxc - minc, maxr - minr, + fill=False, edgecolor='red', linewidth=2) + ax.add_patch(rect) + +plt.show() From e8ddcefae351b7096890c608df559468e7ceb2f5 Mon Sep 17 00:00:00 2001 From: JDWarner Date: Fri, 31 Aug 2012 14:14:46 -0500 Subject: [PATCH 425/648] PEP8 compliance, removed `scaling`, different data parsing. This commit represents all recommended changes since the last commit, notably: * PEP8 compliance (in new sections; a few old ones still noncompliant w/indentations) * Moved `depth` kwarg to end of list and in docstring. Clarified `depth` docstring, and added section in Notes further explaining this parameter. * Added section in Notes warning that for multichannel inputs, all channels are combined during scaling. The user must separately normalize each channel prior to calling random_walker() * New method for parsing data, allowing more elegant gradient calculation code. Probably also more extensible. The 2D multispectral case forced this change. * New test: `test_multispectral_2d()` --- .../random_walker_segmentation.py | 97 ++++++++----------- .../segmentation/tests/test_random_walker.py | 23 ++++- 2 files changed, 61 insertions(+), 59 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 6165770d..52bf4425 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -67,23 +67,17 @@ def _make_graph_edges_3d(n_x, n_y, n_z): def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1., multichannel=False): # Weight calculation is main difference in multispectral version - # Original gradient**2 replaced with sqrt( sum of gradients**2 ) - if not multichannel: - gradients = _compute_gradients_3d( data, depth=depth )**2 - else: - for channel in range(data.shape[-1]): - if channel == 0: - gradients = _compute_gradients_3d(data[..., channel], - depth=depth)**2 - else: - gradients += _compute_gradients_3d(data[..., channel], - depth=depth)**2 + # Original gradient**2 replaced with sum of gradients ** 2 + gradients = 0 + for channel in range(0, data.shape[-1]): + gradients += _compute_gradients_3d(data[..., channel], + depth=depth) ** 2 # All channels considered together in this standard deviation beta /= 10 * data.std() if multichannel: # New final term in beta to give == results in trivial case where # multiple identical spectra are passed. - beta /= np.sqrt( data.shape[-1] ) + beta /= np.sqrt(data.shape[-1]) gradients *= beta weights = np.exp(- gradients) weights += eps @@ -166,10 +160,7 @@ def _mask_edges_weights(edges, weights, mask): def _build_laplacian(data, mask=None, beta=50, depth=1., multichannel=False): - if not multichannel: - l_x, l_y, l_z = data.shape - else: - l_x, l_y, l_z = data.shape[0], data.shape[1], data.shape[2] + l_x, l_y, l_z = data.shape[:3] edges = _make_graph_edges_3d(l_x, l_y, l_z) weights = _compute_weights_3d(data, beta=beta, eps=1.e-10, depth=depth, multichannel=multichannel) @@ -183,21 +174,21 @@ def _build_laplacian(data, mask=None, beta=50, depth=1., multichannel=False): #----------- Random walker algorithm -------------------------------- -def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, - copy=True, multichannel=False, scaling='all', - return_full_prob=False): +def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, + multichannel=False, return_full_prob=False, depth=1.): """ - Multichannel random walker algorithm for segmentation from markers. + Random walker algorithm for segmentation from markers, for gray-level or + multichannel images. Parameters ---------- data : array_like - Image to be segmented in phases. Gray-level`data` can be two- or + Image to be segmented in phases. Gray-level `data` can be two- or three-dimensional; multichannel data can be three- or four- - dimensional (requires multichannel=True) with the highest - dimension denoting channels. Data spacing is assumed isotropic - unless depth keyword argument is used. + dimensional (multichannel=True) with the highest dimension denoting + channels. Data spacing is assumed isotropic unless depth keyword + argument is used. labels : array of ints, of same shape as `data` Array of seed markers labeled with different positive integers @@ -211,11 +202,6 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, Penalization coefficient for the random walker motion (the greater `beta`, the more difficult the diffusion). - depth : float, default 1. - Correction for non-isotropic voxel depths in 3D volumes. - Default (1.) implies isotropy. This factor is derived as follows: - depth = (slice thickness) / (in-plane voxel dimension) - mode : {'bf', 'cg_mg', 'cg'} (default: 'bf') Mode for solving the linear system in the random walker algorithm. @@ -250,21 +236,17 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, If True, input data is parsed as multichannel data (see 'data' above for proper input format in this case) - scaling : string, default 'all' - Controls input scaling if multichannel=True (otherwise no effect). - - - 'all' (default): Data from all channels is combined when scaling - input data to the range [0,1] as type np.float64. Recommended - option for RGB(A) inputs. - - - 'separate': Each channel is scaled individually, separate from the - others, to the range [0,1]. Select this if the channels are very - different, for example if one were x-ray CT and another MRI data. - return_full_prob : bool, default False If True, the probability that a pixel belongs to each of the labels will be returned, instead of only the most likely label. + depth : float, default 1. + Correction for non-isotropic voxel depths in 3D volumes. + Default (1.) implies isotropy. This factor is derived as follows: + depth = (out-of-plane voxel spacing) / (in-plane voxel spacing), where + in-plane voxel spacing represents the first two spatial dimensions and + out-of-plane voxel spacing represents the third spatial dimension. + Returns ------- @@ -286,6 +268,16 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, Notes ----- + Multichannel inputs are scaled with all channel data combined. Ensure all + channels are separately normalized prior to running this algorithm. + + The `depth` argument is specifically for certain types of 3-dimensional + volumes which, due to how they were acquired, have different spacing + along in-plane and out-of-plane dimensions. This is commonly encountered + in medical imaging. The `depth` argument corrects gradients calculated + along the third spatial dimension for the otherwise inherent assumption + that all points are equally spaced. + The algorithm was first proposed in *Random walks for image segmentation*, Leo Grady, IEEE Trans Pattern Anal Mach Intell. 2006 Nov;28(11):1768-83. @@ -346,21 +338,18 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, """ # Parse input data if not multichannel: - # We work with 3-D arrays of floats + # We work with 4-D arrays of floats dims = data.shape - data = np.atleast_3d( img_as_float(data) ) + data = np.atleast_3d(img_as_float(data)) + data.shape += (1,) else: dims = data[..., 0].shape - data = np.atleast_3d( data ) # Should never be needed - if scaling.lower().strip() == 'all': - data = img_as_float( data ) - else: - newdata = np.zeros(data.shape, dtype=np.float64) - for channel in range( data.shape[-1] ): - newdata[..., channel] = img_as_float( data[..., channel] ) - del data - data = newdata - del newdata + assert multichannel and data.ndim > 2, 'For multichannel input, data \ + must have >= 3 dimensions.' + data = img_as_float(data) + if data.ndim == 3: + data.shape += (1,) + data = data.transpose((0, 1, 3, 2)) if copy: labels = np.copy(labels) @@ -409,7 +398,7 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3, if return_full_prob: labels = labels.astype(np.float) X = np.array([_clean_labels_ar(Xline, labels, - copy=True).reshape(dims) for Xline in X]) + copy=True).reshape(dims) for Xline in X]) for i in range(1, int(labels.max()) + 1): mask_i = np.squeeze(labels == i) X[i - 1, mask_i] = 1 @@ -429,7 +418,7 @@ def _solve_bf(lap_sparse, B, return_full_prob=False): lap_sparse = lap_sparse.tocsc() solver = sparse.linalg.factorized(lap_sparse.astype(np.double)) X = np.array([solver(np.array((-B[i]).todense()).ravel())\ - for i in range(len(B))]) + for i in range(len(B))]) if not return_full_prob: X = np.argmax(X, axis=0) return X diff --git a/skimage/segmentation/tests/test_random_walker.py b/skimage/segmentation/tests/test_random_walker.py index 74397dbe..ecf59e99 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -86,6 +86,7 @@ def test_2d_cg_mg(): full_prob[0, 25:45, 40:60]).all() return data, labels_cg_mg + def test_types(): lx = 70 ly = 100 @@ -96,6 +97,7 @@ def test_types(): assert (labels_cg_mg[25:45, 40:60] == 2).all() return data, labels_cg_mg + def test_reorder_labels(): lx = 70 ly = 100 @@ -106,7 +108,6 @@ def test_reorder_labels(): return data, labels_bf - def test_2d_inactive(): lx = 70 ly = 100 @@ -139,14 +140,26 @@ def test_3d_inactive(): return data, labels, old_labels, after_labels -def test_multispectral(): +def test_multispectral_2d(): + lx, ly = 70, 100 + data, labels = make_2d_syntheticdata(lx, ly) + data2 = data.copy() + data.shape += (1,) + data = data.repeat(2, axis=2) # Result should be identical + multi_labels = random_walker(data, labels, mode='cg', multichannel=True) + single_labels = random_walker(data2, labels, mode='cg') + assert (multi_labels.reshape(labels.shape)[25:45, 40:60] == 2).all() + return data, multi_labels, single_labels, labels + + +def test_multispectral_3d(): n = 30 lx, ly, lz = n, n, n - data, labels = make_3d_syntheticdata( lx, ly, lz ) + data, labels = make_3d_syntheticdata(lx, ly, lz) data.shape += (1,) - data = data.repeat(2, axis=3) # Result should be identical + data = data.repeat(2, axis=3) # Result should be identical multi_labels = random_walker(data, labels, mode='cg', multichannel=True) - single_labels = random_walker(data[:,:,:,0], labels, mode='cg') + single_labels = random_walker(data[..., 0], labels, mode='cg') assert (multi_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all() assert (single_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all() return data, multi_labels, single_labels, labels From 6e9d6e28574ce8fe4eb7c26396e7fbc1d0ab5d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 31 Aug 2012 22:25:10 +0200 Subject: [PATCH 426/648] Improve visualization of labelling --- doc/examples/plot_label.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/doc/examples/plot_label.py b/doc/examples/plot_label.py index 7b2fd717..ab57d8e9 100644 --- a/doc/examples/plot_label.py +++ b/doc/examples/plot_label.py @@ -7,13 +7,14 @@ This example shows how to segment an image with image labelling. """ +import numpy as np import matplotlib.pyplot as plt import matplotlib.patches as mpatches from skimage import data from skimage.filter import threshold_otsu from skimage.segmentation import clear_border -from skimage.morphology import label +from skimage.morphology import label, closing, square from skimage.measure import regionprops @@ -21,11 +22,7 @@ image = data.coins()[50:-50, 50:-50] # apply threshold thresh = threshold_otsu(image) -bw = image > thresh - -fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) -plt.gray() -ax.imshow(bw) +bw = closing(image > thresh, square(3)) # remove artifacts connected to image border cleared = bw.copy() @@ -33,6 +30,11 @@ clear_border(cleared) # label image regions label_image = label(cleared) +borders = np.logical_xor(bw, cleared) +label_image[borders] = -1 + +fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) +ax.imshow(label_image) for region in regionprops(label_image, ['Area', 'BoundingBox']): From c9291718f9912a1ce402a47a828a5e0bad8ed74e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 31 Aug 2012 22:30:09 +0200 Subject: [PATCH 427/648] Update description with more detailed explanation of applied steps --- doc/examples/plot_label.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/examples/plot_label.py b/doc/examples/plot_label.py index ab57d8e9..e9e47831 100644 --- a/doc/examples/plot_label.py +++ b/doc/examples/plot_label.py @@ -3,7 +3,13 @@ Label image regions =================== -This example shows how to segment an image with image labelling. +This example shows how to segment an image with image labelling. The following +steps are applied: + +1. Thresholding with automatic Otsu method +2. Close small holes with binary closing +3. Remove artifacts touching image border +4. Measure image regions to filter small objects """ From f360316f17c6fbc36a3ab3d76f676d4cb6466d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 31 Aug 2012 22:55:24 +0200 Subject: [PATCH 428/648] Explicitly define colormap --- doc/examples/plot_label.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_label.py b/doc/examples/plot_label.py index e9e47831..8c46cb8e 100644 --- a/doc/examples/plot_label.py +++ b/doc/examples/plot_label.py @@ -40,7 +40,7 @@ borders = np.logical_xor(bw, cleared) label_image[borders] = -1 fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) -ax.imshow(label_image) +ax.imshow(label_image, cmap='jet') for region in regionprops(label_image, ['Area', 'BoundingBox']): From b2e4fd6f32630aadf51d1004e34abf49f67aea0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 31 Aug 2012 23:46:23 +0200 Subject: [PATCH 429/648] Add parallel execution support --- skimage/_shared/interpolation.pxd | 16 ++++++++-------- skimage/_shared/interpolation.pyx | 16 ++++++++-------- skimage/transform/_warps_cy.pyx | 16 ++++++++++------ skimage/transform/setup.py | 4 +++- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/skimage/_shared/interpolation.pxd b/skimage/_shared/interpolation.pxd index ef880109..6038a853 100644 --- a/skimage/_shared/interpolation.pxd +++ b/skimage/_shared/interpolation.pxd @@ -2,23 +2,23 @@ cdef inline double nearest_neighbour_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval) + double cval) nogil cdef inline double bilinear_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval) + double cval) nogil -cdef inline double quadratic_interpolation(double x, double[3] f) +cdef inline double quadratic_interpolation(double x, double[3] f) nogil cdef inline double biquadratic_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval) + double cval) nogil -cdef inline double cubic_interpolation(double x, double[4] f) +cdef inline double cubic_interpolation(double x, double[4] f) nogil cdef inline double bicubic_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval) + double cval) nogil cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, - char mode, double cval) + char mode, double cval) nogil -cdef inline int coord_map(int dim, int coord, char mode) +cdef inline int coord_map(int dim, int coord, char mode) nogil diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx index 3150f41b..231ca045 100644 --- a/skimage/_shared/interpolation.pyx +++ b/skimage/_shared/interpolation.pyx @@ -8,7 +8,7 @@ from libc.math cimport ceil, floor, round cdef inline double nearest_neighbour_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval): + double cval) nogil: """Nearest neighbour interpolation at a given position in the image. Parameters @@ -37,7 +37,7 @@ cdef inline double nearest_neighbour_interpolation(double* image, int rows, cdef inline double bilinear_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval): + double cval) nogil: """Bilinear interpolation at a given position in the image. Parameters @@ -75,7 +75,7 @@ cdef inline double bilinear_interpolation(double* image, int rows, int cols, return (1 - dr) * top + dr * bottom -cdef inline double quadratic_interpolation(double x, double[3] f): +cdef inline double quadratic_interpolation(double x, double[3] f) nogil: """Quadratic interpolation. Parameters @@ -96,7 +96,7 @@ cdef inline double quadratic_interpolation(double x, double[3] f): cdef inline double biquadratic_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval): + double cval) nogil: """Biquadratic interpolation at a given position in the image. Parameters @@ -147,7 +147,7 @@ cdef inline double biquadratic_interpolation(double* image, int rows, int cols, return quadratic_interpolation(xr, fr) -cdef inline double cubic_interpolation(double x, double[4] f): +cdef inline double cubic_interpolation(double x, double[4] f) nogil: """Cubic interpolation. Parameters @@ -172,7 +172,7 @@ cdef inline double cubic_interpolation(double x, double[4] f): cdef inline double bicubic_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval): + double cval) nogil: """Bicubic interpolation at a given position in the image. Parameters @@ -220,7 +220,7 @@ cdef inline double bicubic_interpolation(double* image, int rows, int cols, cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, - char mode, double cval): + char mode, double cval) nogil: """Get a pixel from the image, taking wrapping mode into consideration. Parameters @@ -251,7 +251,7 @@ cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, return image[coord_map(rows, r, mode) * cols + coord_map(cols, c, mode)] -cdef inline int coord_map(int dim, int coord, char mode): +cdef inline int coord_map(int dim, int coord, char mode) nogil: """ Wrap a coordinate, according to a given mode. diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index ce400ed6..9d3dca70 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -5,6 +5,7 @@ cimport numpy as np import numpy as np +from cython.parallel import prange from skimage._shared.interpolation cimport (nearest_neighbour_interpolation, bilinear_interpolation, biquadratic_interpolation, @@ -12,7 +13,7 @@ from skimage._shared.interpolation cimport (nearest_neighbour_interpolation, cdef inline void _matrix_transform(double x, double y, double* H, double *x_, - double *y_): + double *y_) nogil: """Apply a homography to a coordinate. Parameters @@ -101,8 +102,9 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, out_r = output_shape[0] out_c = output_shape[1] - cdef np.ndarray[dtype=np.double_t, ndim=2] out = \ + cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] out = \ np.zeros((out_r, out_c), dtype=np.double) + cdef double* out_data = out.data cdef int tfr, tfc cdef double r, c @@ -110,7 +112,7 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, cdef int cols = img.shape[1] cdef double (*interp_func)(double*, int, int, double, double, - char, double) + char, double) nogil if order == 0: interp_func = nearest_neighbour_interpolation elif order == 1: @@ -120,10 +122,12 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, elif order == 3: interp_func = bicubic_interpolation - for tfr in range(out_r): + for tfr in prange(out_r, nogil=True): + # make r, c thread local variables + r = c = 0 for tfc in range(out_c): _matrix_transform(tfc, tfr, M.data, &c, &r) - out[tfr, tfc] = interp_func(img.data, rows, cols, r, c, - mode_c, cval) + out_data[tfr * out_r + tfc] = interp_func(img.data, rows, + cols, r, c, mode_c, cval) return out diff --git a/skimage/transform/setup.py b/skimage/transform/setup.py index 0e415bad..cd2a0f8a 100644 --- a/skimage/transform/setup.py +++ b/skimage/transform/setup.py @@ -20,7 +20,9 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('_warps_cy', sources=['_warps_cy.c'], - include_dirs=[get_numpy_include_dirs(), '../_shared']) + include_dirs=[get_numpy_include_dirs(), '../_shared'], + extra_compile_args=['-fopenmp'], + extra_link_args=['-fopenmp']) return config From 06d9f7110fef631dcdbff6699ed896e3f8a9c444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 1 Sep 2012 09:09:25 +0200 Subject: [PATCH 430/648] Revert to non parallelized execution --- skimage/_shared/interpolation.pxd | 16 ++++++++-------- skimage/_shared/interpolation.pyx | 16 ++++++++-------- skimage/transform/_warps_cy.pyx | 16 ++++++---------- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/skimage/_shared/interpolation.pxd b/skimage/_shared/interpolation.pxd index 6038a853..ef880109 100644 --- a/skimage/_shared/interpolation.pxd +++ b/skimage/_shared/interpolation.pxd @@ -2,23 +2,23 @@ cdef inline double nearest_neighbour_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval) nogil + double cval) cdef inline double bilinear_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval) nogil + double cval) -cdef inline double quadratic_interpolation(double x, double[3] f) nogil +cdef inline double quadratic_interpolation(double x, double[3] f) cdef inline double biquadratic_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval) nogil + double cval) -cdef inline double cubic_interpolation(double x, double[4] f) nogil +cdef inline double cubic_interpolation(double x, double[4] f) cdef inline double bicubic_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval) nogil + double cval) cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, - char mode, double cval) nogil + char mode, double cval) -cdef inline int coord_map(int dim, int coord, char mode) nogil +cdef inline int coord_map(int dim, int coord, char mode) diff --git a/skimage/_shared/interpolation.pyx b/skimage/_shared/interpolation.pyx index 231ca045..3150f41b 100644 --- a/skimage/_shared/interpolation.pyx +++ b/skimage/_shared/interpolation.pyx @@ -8,7 +8,7 @@ from libc.math cimport ceil, floor, round cdef inline double nearest_neighbour_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval) nogil: + double cval): """Nearest neighbour interpolation at a given position in the image. Parameters @@ -37,7 +37,7 @@ cdef inline double nearest_neighbour_interpolation(double* image, int rows, cdef inline double bilinear_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval) nogil: + double cval): """Bilinear interpolation at a given position in the image. Parameters @@ -75,7 +75,7 @@ cdef inline double bilinear_interpolation(double* image, int rows, int cols, return (1 - dr) * top + dr * bottom -cdef inline double quadratic_interpolation(double x, double[3] f) nogil: +cdef inline double quadratic_interpolation(double x, double[3] f): """Quadratic interpolation. Parameters @@ -96,7 +96,7 @@ cdef inline double quadratic_interpolation(double x, double[3] f) nogil: cdef inline double biquadratic_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval) nogil: + double cval): """Biquadratic interpolation at a given position in the image. Parameters @@ -147,7 +147,7 @@ cdef inline double biquadratic_interpolation(double* image, int rows, int cols, return quadratic_interpolation(xr, fr) -cdef inline double cubic_interpolation(double x, double[4] f) nogil: +cdef inline double cubic_interpolation(double x, double[4] f): """Cubic interpolation. Parameters @@ -172,7 +172,7 @@ cdef inline double cubic_interpolation(double x, double[4] f) nogil: cdef inline double bicubic_interpolation(double* image, int rows, int cols, double r, double c, char mode, - double cval) nogil: + double cval): """Bicubic interpolation at a given position in the image. Parameters @@ -220,7 +220,7 @@ cdef inline double bicubic_interpolation(double* image, int rows, int cols, cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, - char mode, double cval) nogil: + char mode, double cval): """Get a pixel from the image, taking wrapping mode into consideration. Parameters @@ -251,7 +251,7 @@ cdef inline double get_pixel(double* image, int rows, int cols, int r, int c, return image[coord_map(rows, r, mode) * cols + coord_map(cols, c, mode)] -cdef inline int coord_map(int dim, int coord, char mode) nogil: +cdef inline int coord_map(int dim, int coord, char mode): """ Wrap a coordinate, according to a given mode. diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index 9d3dca70..ce400ed6 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -5,7 +5,6 @@ cimport numpy as np import numpy as np -from cython.parallel import prange from skimage._shared.interpolation cimport (nearest_neighbour_interpolation, bilinear_interpolation, biquadratic_interpolation, @@ -13,7 +12,7 @@ from skimage._shared.interpolation cimport (nearest_neighbour_interpolation, cdef inline void _matrix_transform(double x, double y, double* H, double *x_, - double *y_) nogil: + double *y_): """Apply a homography to a coordinate. Parameters @@ -102,9 +101,8 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, out_r = output_shape[0] out_c = output_shape[1] - cdef np.ndarray[dtype=np.double_t, ndim=2, mode="c"] out = \ + cdef np.ndarray[dtype=np.double_t, ndim=2] out = \ np.zeros((out_r, out_c), dtype=np.double) - cdef double* out_data = out.data cdef int tfr, tfc cdef double r, c @@ -112,7 +110,7 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, cdef int cols = img.shape[1] cdef double (*interp_func)(double*, int, int, double, double, - char, double) nogil + char, double) if order == 0: interp_func = nearest_neighbour_interpolation elif order == 1: @@ -122,12 +120,10 @@ def _warp_fast(np.ndarray image, np.ndarray H, output_shape=None, int order=1, elif order == 3: interp_func = bicubic_interpolation - for tfr in prange(out_r, nogil=True): - # make r, c thread local variables - r = c = 0 + for tfr in range(out_r): for tfc in range(out_c): _matrix_transform(tfc, tfr, M.data, &c, &r) - out_data[tfr * out_r + tfc] = interp_func(img.data, rows, - cols, r, c, mode_c, cval) + out[tfr, tfc] = interp_func(img.data, rows, cols, r, c, + mode_c, cval) return out From 74797d62057b1525a22feb381409b670819379b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 1 Sep 2012 09:17:16 +0200 Subject: [PATCH 431/648] Fix decision whether to use warping or fast warping --- skimage/transform/_geometric.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 270198aa..44bc8651 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -832,6 +832,8 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, ishape = np.array(image.shape) bands = ishape[2] + out = None + # use fast Cython version for specific interpolation orders if order in range(4) and not map_args: matrix = None @@ -851,7 +853,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, if orig_ndim == 2: out = out[..., 0] - else: # use ndimage.map_coordinates + if out is None: # use ndimage.map_coordinates if output_shape is None: output_shape = ishape From b05c062d2490c9be607ec69d8302aa6dc9d4dff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 1 Sep 2012 14:54:17 +0200 Subject: [PATCH 432/648] Refactor erosion and dilation for better performance --- skimage/morphology/cmorph.pyx | 172 +++++++++++++++++----------------- skimage/morphology/grey.py | 24 ++--- 2 files changed, 94 insertions(+), 102 deletions(-) diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index 6870b500..5e5a88f9 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -1,116 +1,120 @@ -""" -:author: Damian Eads, 2009 -:license: modified BSD -""" +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False -from __future__ import division import numpy as np - cimport numpy as np cimport cython from cpython cimport bool +from libc.stdlib cimport malloc, free -STREL_DTYPE = np.uint8 -ctypedef np.uint8_t STREL_DTYPE_t -IMAGE_DTYPE = np.uint8 -ctypedef np.uint8_t IMAGE_DTYPE_t +def dilate(np.ndarray[np.uint8_t, ndim=2] image, + np.ndarray[np.uint8_t, ndim=2] selem, + np.ndarray[np.uint8_t, ndim=2] out=None, + shift_x=False, shift_y=False): -cdef inline int int_max(int a, int b): return a if a >= b else b -cdef inline int int_min(int a, int b): return a if a <= b else b + cdef int rows = image.shape[0] + cdef int cols = image.shape[1] + cdef int srows = selem.shape[0] + cdef int scols = selem.shape[1] -@cython.boundscheck(False) -def dilate(np.ndarray[IMAGE_DTYPE_t, ndim=2] image not None, - np.ndarray[IMAGE_DTYPE_t, ndim=2] selem not None, - np.ndarray[IMAGE_DTYPE_t, ndim=2] out, - bool shift_x, bool shift_y): - cdef int hw = selem.shape[0] // 2 - cdef int hh = selem.shape[1] // 2 - if shift_x: - hh -= 1 - if shift_y: - hw -= 1 + cdef int centre_r = int(selem.shape[0] / 2) - shift_y + cdef int centre_c = int(selem.shape[1] / 2) - shift_x - cdef int width = image.shape[0], height = image.shape[1] + image = np.ascontiguousarray(image) if out is None: - out = np.zeros([width, height], dtype=IMAGE_DTYPE) + out = np.zeros((rows, cols), dtype=np.uint8) + else: + out = np.ascontiguousarray(out) - assert out.shape[0] == image.shape[0] - assert out.shape[1] == image.shape[1] + cdef np.uint8_t* out_data = out.data + cdef np.uint8_t* image_data = image.data - cdef int x, y, ix, iy, cx, cy - cdef IMAGE_DTYPE_t max_so_far + cdef int r, c, rr, cc, s, value, local_max - cdef int sw = selem.shape[0], sh = selem.shape[1] + cdef int selem_num = np.sum(selem != 0) + cdef int* sr = malloc(selem_num * sizeof(int)) + cdef int* sc = malloc(selem_num * sizeof(int)) - cdef np.ndarray[np.int_t, ndim=2] xinc = np.zeros([sw, sh], dtype=np.int) - cdef np.ndarray[np.int_t, ndim=2] yinc = np.zeros([sw, sh], dtype=np.int) + s = 0 + for r in range(srows): + for c in range(scols): + if selem[r, c] != 0: + sr[s] = r - centre_r + sc[s] = c - centre_c + s += 1 - for x in range(sw): - for y in range(sh): - xinc[x, y] = (x - hw) - yinc[x, y] = (y - hh) + for r in range(rows): + for c in range(cols): + local_max = 0 + for s in range(selem_num): + rr = r + sr[s] + cc = c + sc[s] + if 0 <= rr < rows and 0 <= cc < cols: + value = image_data[rr * rows + cc] + if value > local_max: + local_max = value + out_data[r * cols + c] = local_max - for x in range(width): - for y in range(height): - max_so_far = 0 - for cx in range(0, sw): - for cy in range(0, sh): - ix = x + xinc[cx,cy] - iy = y + yinc[cx,cy] - if ix>=0 and iy>=0 and ix < width and iy < height \ - and selem[cx, cy] == 1 \ - and image[ix,iy] > max_so_far: - max_so_far = image[ix,iy] - out[x,y] = max_so_far + free(sr) + free(sc) return out -@cython.boundscheck(False) -def erode(np.ndarray[IMAGE_DTYPE_t, ndim=2] image not None, - np.ndarray[IMAGE_DTYPE_t, ndim=2] selem not None, - np.ndarray[IMAGE_DTYPE_t, ndim=2] out, - bool shift_x, bool shift_y): - cdef int hw = selem.shape[0] // 2 - cdef int hh = selem.shape[1] // 2 - if shift_x: - hh -= 1 - if shift_y: - hw -= 1 +def erode(np.ndarray[np.uint8_t, ndim=2] image, + np.ndarray[np.uint8_t, ndim=2] selem, + np.ndarray[np.uint8_t, ndim=2] out=None, + shift_x=False, shift_y=False): - cdef int width = image.shape[0], height = image.shape[1] + cdef int rows = image.shape[0] + cdef int cols = image.shape[1] + cdef int srows = selem.shape[0] + cdef int scols = selem.shape[1] + + cdef int centre_r = int(selem.shape[0] / 2) - shift_y + cdef int centre_c = int(selem.shape[1] / 2) - shift_x + + image = np.ascontiguousarray(image) if out is None: - out = np.zeros([width, height], dtype=IMAGE_DTYPE) + out = np.zeros((rows, cols), dtype=np.uint8) + else: + out = np.ascontiguousarray(out) - assert out.shape[0] == image.shape[0] - assert out.shape[1] == image.shape[1] + cdef np.uint8_t* out_data = out.data + cdef np.uint8_t* image_data = image.data - cdef int x, y, ix, iy, cx, cy - cdef IMAGE_DTYPE_t min_so_far + cdef int r, c, rr, cc, s, value, local_max - cdef int sw = selem.shape[0], sh = selem.shape[1] + cdef int selem_num = np.sum(selem != 0) + cdef int* sr = malloc(selem_num * sizeof(int)) + cdef int* sc = malloc(selem_num * sizeof(int)) - cdef np.ndarray[np.int_t, ndim=2] xinc = np.zeros([sw, sh], dtype=np.int) - cdef np.ndarray[np.int_t, ndim=2] yinc = np.zeros([sw, sh], dtype=np.int) + s = 0 + for r in range(srows): + for c in range(scols): + if selem[r, c] != 0: + sr[s] = r - centre_r + sc[s] = c - centre_c + s += 1 - for x in range(sw): - for y in range(sh): - xinc[x, y] = (x - hw) - yinc[x, y] = (y - hh) + for r in range(rows): + for c in range(cols): + local_min = 255 + for s in range(selem_num): + rr = r + sr[s] + cc = c + sc[s] + if 0 <= rr < rows and 0 <= cc < cols: + value = image_data[rr * rows + cc] + if value < local_min: + local_min = value - for x in range(width): - for y in range(height): - min_so_far = 255 - for cx in range(0, sw): - for cy in range(0, sh): - ix = x + xinc[cx,cy] - iy = y + yinc[cx,cy] - if ix>=0 and iy>=0 and ix < width \ - and iy < height and selem[cx, cy] == 1 \ - and image[ix,iy] < min_so_far: - min_so_far = image[ix,iy] - out[x,y] = min_so_far + out_data[r * cols + c] = local_min + + free(sr) + free(sc) return out diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 309fb5be..7ce25475 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -6,11 +6,11 @@ __docformat__ = 'restructuredtext en' import warnings - import numpy as np - import skimage +from . import cmorph + __all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat', 'black_tophat', 'greyscale_erode', 'greyscale_dilate', @@ -66,14 +66,8 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): if image is out: raise NotImplementedError("In-place erosion not supported!") image = skimage.img_as_ubyte(image) - - try: - import skimage.morphology.cmorph as cmorph - out = cmorph.erode(image, selem, out=out, - shift_x=shift_x, shift_y=shift_y) - return out - except ImportError: - raise ImportError("cmorph extension not available.") + return cmorph.erode(image, selem, out=out, + shift_x=shift_x, shift_y=shift_y) def dilation(image, selem, out=None, shift_x=False, shift_y=False): @@ -125,14 +119,8 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): if image is out: raise NotImplementedError("In-place dilation not supported!") image = skimage.img_as_ubyte(image) - - try: - from . import cmorph - out = cmorph.dilate(image, selem, out=out, - shift_x=shift_x, shift_y=shift_y) - return out - except ImportError: - raise ImportError("cmorph extension not available.") + return cmorph.dilate(image, selem, out=out, + shift_x=shift_x, shift_y=shift_y) def opening(image, selem, out=None): From a5fe574bd954e35a000d12f75df9d9cf4cc6cbfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 1 Sep 2012 14:57:56 +0200 Subject: [PATCH 433/648] Remove unused imports and add missing types --- skimage/morphology/cmorph.pyx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index 5e5a88f9..fb1e3af1 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -5,15 +5,13 @@ import numpy as np cimport numpy as np -cimport cython -from cpython cimport bool from libc.stdlib cimport malloc, free def dilate(np.ndarray[np.uint8_t, ndim=2] image, np.ndarray[np.uint8_t, ndim=2] selem, np.ndarray[np.uint8_t, ndim=2] out=None, - shift_x=False, shift_y=False): + char shift_x=0, char shift_y=0): cdef int rows = image.shape[0] cdef int cols = image.shape[1] @@ -68,7 +66,7 @@ def dilate(np.ndarray[np.uint8_t, ndim=2] image, def erode(np.ndarray[np.uint8_t, ndim=2] image, np.ndarray[np.uint8_t, ndim=2] selem, np.ndarray[np.uint8_t, ndim=2] out=None, - shift_x=False, shift_y=False): + char shift_x=0, char shift_y=0): cdef int rows = image.shape[0] cdef int cols = image.shape[1] From 6facfd27a16717d4ec443b01b7efc48a4b36af88 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 1 Sep 2012 10:25:17 -0400 Subject: [PATCH 434/648] BUG: Bento version must end with number --- bento.info | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bento.info b/bento.info index 2f32afd4..a23e83f0 100644 --- a/bento.info +++ b/bento.info @@ -1,5 +1,5 @@ Name: scikits-image -Version: 0.7.0.dev +Version: 0.7.0.dev0 Summary: Image processing routines for SciPy Url: http://scikits-image.org DownloadUrl: http://github.com/scikits-image/scikits-image From 2314cfd8d6c42fd6f374a27140031b19001fbb9e Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 1 Sep 2012 10:32:29 -0400 Subject: [PATCH 435/648] ENH: Add script to check that bento.info is up-to-date. --- check_bento_build.py | 95 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 check_bento_build.py diff --git a/check_bento_build.py b/check_bento_build.py new file mode 100644 index 00000000..d085b3ba --- /dev/null +++ b/check_bento_build.py @@ -0,0 +1,95 @@ +""" +Check that Cython extensions in setup.py files match those in bento.info. +""" +import os +import re + + +RE_CYTHON = re.compile("config.add_extension\(['\"]([\S]+)['\"]") + +BENTO_TEMPLATE = """ + Extension: {module_path} + Sources: + {dir_path}.pyx""" + + +def each_setup_in_pkg(top_dir): + """Yield path and file object for each setup.py file""" + for dir_path, dir_names, filenames in os.walk(top_dir): + for fname in filenames: + if fname == 'setup.py': + with open(os.path.join(dir_path, 'setup.py')) as f: + yield dir_path, f + + +def each_cy_in_setup(top_dir): + """Yield path and name for each cython extension package's setup file.""" + for dir_path, f in each_setup_in_pkg(top_dir): + text = f.read() + match = RE_CYTHON.findall(text) + if match: + for cy_file in match: + # if cython files in different directory than setup.py + if '.' in cy_file: + parts = cy_file.split('.') + cy_file = parts[-1] + # Don't overwrite dir_path for subsequent iterations. + path = os.path.join(dir_path, *parts[:-1]) + else: + path = dir_path + full_path = os.path.join(path, cy_file) + yield full_path, cy_file + + +def each_cy_in_bento(bento_file='bento.info'): + """Yield path and name for each cython extension in bento info file.""" + with open(bento_file) as f: + for line in f: + line = line.strip() + if line.startswith('Extension:'): + parts = line.split('.') + ext_name = parts[-1] + path = line.lstrip('Extension:').strip() + yield path, ext_name + + +def remove_common_extensions(cy_bento, cy_setup): + for ext_name in cy_bento.keys(): + if ext_name in cy_setup: + spath = cy_setup.pop(ext_name) + bpath = cy_bento.pop(ext_name) + if not spath.replace(os.path.sep, '.') == bpath: + print "Mismatched paths:" + print " setup.py: ", spath + print " bento.info:", bpath + +def print_results(cy_bento, cy_setup): + def info(text): + print + print(text) + print('-' * len(text)) + + print # blank line; just for aesthetics + + if cy_bento: + info("The following extensions in 'bento.info' were not found:") + print('\n'.join(cy_bento.keys())) + + + if cy_setup: + info("The following cython files exist but were not in 'bento.info':") + print('\n'.join(cy_setup)) + info("Consider adding the following to the 'bento.info' Library:") + for ext_name, dir_path in cy_setup.iteritems(): + print BENTO_TEMPLATE.format(module_path=dir_path.replace('/', '.'), + dir_path=dir_path) + +if __name__ == '__main__': + # All cython extensions defined in 'setup.py' files. + cy_setup = dict((ext, path) for path, ext in each_cy_in_setup('skimage')) + + # All cython extensions defined 'bento.info' file. + cy_bento = dict((ext, path) for path, ext in each_cy_in_bento()) + + remove_common_extensions(cy_bento, cy_setup) + print_results(cy_bento, cy_setup) From ed05b8874015bbe504453b21d7dcf688852de387 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 1 Sep 2012 10:52:15 -0400 Subject: [PATCH 436/648] BUG: Update bento.info to match setup.py files --- bento.info | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/bento.info b/bento.info index a23e83f0..4365cf04 100644 --- a/bento.info +++ b/bento.info @@ -40,9 +40,6 @@ Library: Extension: skimage.morphology._pnpoly Sources: skimage/morphology/_pnpoly.pyx - Extension: skimage.feature._greycomatrix - Sources: - skimage/feature/_greycomatrix.pyx Extension: skimage.feature._template Sources: skimage/feature/_template.pyx @@ -76,15 +73,9 @@ Library: Extension: skimage.morphology._convex_hull Sources: skimage/morphology/_convex_hull.pyx - Extension: skimage.morphology._skeletonize - Sources: - skimage/morphology/_skeletonize.pyx Extension: skimage.draw._draw Sources: skimage/draw/_draw.pyx - Extension: skimage.transform._project - Sources: - skimage/transform/_project.pyx Extension: skimage.graph._spath Sources: skimage/graph/_spath.pyx @@ -94,6 +85,36 @@ Library: Extension: skimage.graph.heap Sources: skimage/graph/heap.pyx + Extension: skimage.morphology._greyreconstruct + Sources: + skimage/morphology/_greyreconstruct.pyx + Extension: skimage.feature._texture + Sources: + skimage/feature/_texture.pyx + Extension: skimage._shared.transform + Sources: + skimage/_shared/transform.pyx + Extension: skimage.segmentation._slic + Sources: + skimage/segmentation/_slic.pyx + Extension: skimage.segmentation._quickshift + Sources: + skimage/segmentation/_quickshift.pyx + Extension: skimage.morphology._skeletonize_cy + Sources: + skimage/morphology/_skeletonize_cy.pyx + 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 + Extension: skimage._shared.geometry + Sources: + skimage/_shared/geometry.pyx Executable: skivi Module: skimage.scripts.skivi From 7d858a8ba9c5b03e84d7c4ac4d9e26c87e0624b5 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 1 Aug 2012 00:24:14 -0400 Subject: [PATCH 437/648] ENH: Add CollectionViewer --- skimage/viewer/__init__.py | 2 +- skimage/viewer/viewers/__init__.py | 2 +- skimage/viewer/viewers/core.py | 87 ++++++++++++++++++++ viewer_examples/viewers/collection_viewer.py | 29 +++++++ 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 viewer_examples/viewers/collection_viewer.py diff --git a/skimage/viewer/__init__.py b/skimage/viewer/__init__.py index 638cf43f..cfbeb0c0 100644 --- a/skimage/viewer/__init__.py +++ b/skimage/viewer/__init__.py @@ -1,4 +1,4 @@ try: - from viewers import ImageViewer + from viewers import ImageViewer, CollectionViewer except ImportError: print("Could not import PyQt4 -- ImageViewer not available.") diff --git a/skimage/viewer/viewers/__init__.py b/skimage/viewer/viewers/__init__.py index bb67a43f..a8339edc 100644 --- a/skimage/viewer/viewers/__init__.py +++ b/skimage/viewer/viewers/__init__.py @@ -1 +1 @@ -from .core import * +from .core import ImageViewer, CollectionViewer diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 156f08bd..d29f950e 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -12,6 +12,10 @@ except ImportError: from skimage.util.dtype import dtype_range from ..utils import figimage, MatplotlibCanvas +from ..widgets import Slider + + +__all__ = ['ImageViewer', 'CollectionViewer'] qApp = None @@ -194,3 +198,86 @@ class ImageViewer(QMainWindow): return "%4s @ [%4s, %4s]" % (self.image[y, x], x, y) except IndexError: return "" + + +class CollectionViewer(ImageViewer): + """Viewer for displaying image collections. + + Select the displayed frame of the image collection using the slider or + with the following keyboard shortcuts: + + left/right arrows + Previous/next image in collection. + number keys, 0--9 + 0% to 90% of collection. For example, "5" goes to the image in the + middle (i.e. 50%) of the collection. + home/end keys + First/last image in collection. + + Subclasses and plugins will likely extend the `update_image` method to add + custom overlays or filter the displayed image. + + Parameters + ---------- + image_collection : list of images + List of images to be displayed. + update_on : {'on_slide' | 'on_release'} + Control whether image is updated on slide or release of the image + slider. Using 'on_release' will give smoother behavior when displaying + large images or when writing a plugin/subclass that requires heavy + computation. + """ + + def __init__(self, image_collection, update_on='move', **kwargs): + self.image_collection = image_collection + self.index = 0 + self.num_images = len(self.image_collection) + + first_image = image_collection[0] + super(CollectionViewer, self).__init__(first_image) + + slider_kws = dict(value=0, low=0, high=self.num_images-1) + slider_kws['update_on'] = update_on + slider_kws['callback'] = self.update_index + slider_kws['value_type'] = 'int' + self.slider = Slider('frame', **slider_kws) + self.layout.addWidget(self.slider) + + #TODO: Adjust height to accomodate slider; the following doesn't work + # s_size = self.slider.sizeHint() + # cs_size = self.canvas.sizeHint() + # self.resize(cs_size.width(), cs_size.height() + s_size.height()) + + def update_index(self, name, index): + """Select image on display using index into image collection.""" + index = int(round(index)) + + if index == self.index: + return + + # clip index value to collection limits + index = max(index, 0) + index = min(index, self.num_images-1) + + self.index = index + self.slider.val = index + self.update_image(self.image_collection[index]) + + def update_image(self, image): + """Update displayed image. + + This method can be overridden or extended in subclasses and plugins to + react to image changes. + """ + self.image = image + + def keyPressEvent(self, event): + if type(event) == QtGui.QKeyEvent: + key = event.key() + # Number keys (code: 0 = key 48, 9 = key 57) move to deciles + if 48 <= key < 58: + index = 0.1 * int(key - 48) * self.num_images + self.update_index('', index) + event.accept() + else: + event.ignore() diff --git a/viewer_examples/viewers/collection_viewer.py b/viewer_examples/viewers/collection_viewer.py new file mode 100644 index 00000000..b36ddf82 --- /dev/null +++ b/viewer_examples/viewers/collection_viewer.py @@ -0,0 +1,29 @@ +""" +===================== +CollectionViewer demo +===================== + +Demo of CollectionViewer for viewing collections of images. This demo uses +successively darker versions of the same image to fake an image collection. + +You can scroll through images with the slider, or you can interact with the +viewer using your keyboard: + +left/right arrows + Previous/next image in collection. +number keys, 0--9 + 0% to 90% of collection. For example, "5" goes to the image in the + middle (i.e. 50%) of the collection. +home/end keys + First/last image in collection. + +""" +import numpy as np +from skimage import data +from skimage.viewer import CollectionViewer + +img = data.lena() +img_collection = [np.uint8(img * 0.9**i) for i in range(20)] + +view = CollectionViewer(img_collection) +view.show() From 41ea3ba7fd3af2ae95e424230eea946c4443663c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 28 Jul 2012 11:29:28 -0400 Subject: [PATCH 438/648] Add line profile plugin back in. I saved a copy of the line profile plugin in a branch and then deleted the plugin from the main qtmpl-viewer branch. Unfortunately, I forgot that rebasing off the main branch would erase the plugin. This commit just adds the plugin back. --- skimage/viewer/plugins/lineprofile.py | 241 +++++++++++++++++++++++++ skimage/viewer/plugins/plotplugin.py | 50 +++++ viewer_examples/plugins/lineprofile.py | 9 + 3 files changed, 300 insertions(+) create mode 100644 skimage/viewer/plugins/lineprofile.py create mode 100644 skimage/viewer/plugins/plotplugin.py create mode 100644 viewer_examples/plugins/lineprofile.py diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py new file mode 100644 index 00000000..eaedb90c --- /dev/null +++ b/skimage/viewer/plugins/lineprofile.py @@ -0,0 +1,241 @@ +import numpy as np +import scipy.ndimage as ndi +from skimage.util.dtype import dtype_range + +from .plotplugin import PlotPlugin + + +__all__ = ['LineProfile'] + +#TODO: Extract line tool and add it to a new `canvastools` subpackage. + +class LineProfile(PlotPlugin): + """Plugin to compute interpolated intensity under a scan line on an image. + + See PlotPlugin and Plugin classes for additional details. + + Parameters + ---------- + linewidth : float + Line width for interpolation. Wider lines average over more pixels. + epsilon : float + Maximum pixel distance allowed when selecting end point of scan line. + limits : tuple or {None, 'image', 'dtype'} + (minimum, maximum) intensity limits for plotted profile. The following + special values are defined: + + None : rescale based on min/max intensity along selected scan line. + 'image' : fixed scale based on min/max intensity in image. + 'dtype' : fixed scale based on min/max intensity of image dtype. + """ + name = 'Line Profile' + draws_on_image = True + + def __init__(self, linewidth=1, epsilon=5, limits='image', **kwargs): + super(LineProfile, self).__init__(**kwargs) + self.linewidth = linewidth + self.epsilon = epsilon + self._active_pt = None + self._limit_type = limits + self.line_kwargs = dict(color='y', lw=linewidth, alpha=0.5, marker='s', + markersize=5, solid_capstyle='butt') + print self.help() + + def attach(self, image_viewer): + super(LineProfile, self).attach(image_viewer) + + image = image_viewer.original_image + + if self._limit_type == 'image': + self.limits = (np.min(image), np.max(image)) + elif self._limit_type == 'dtype': + self.self._limit_type = dtype_range[image.dtype.type] + elif self._limit_type is None or len(self._limit_type) == 2: + self.limits = self._limit_type + else: + raise ValueError("Unrecognized `limits`: %s" % self._limit_type) + + if not self._limit_type is None: + self.ax.set_ylim(self.limits) + + h, w = image.shape + self._init_end_pts = np.array([[w/3, h/2], [2*w/3, h/2]]) + self.end_pts = self._init_end_pts.copy() + + x, y = np.transpose(self.end_pts) + self.scan_line = image_viewer.ax.plot(x, y, **self.line_kwargs)[0] + self.artists.append(self.scan_line) + + scan_data = profile_line(image, self.end_pts) + self.profile = self.ax.plot(scan_data, 'k-')[0] + self._autoscale_view() + + self.connect_image_event('key_press_event', self.on_key_press) + self.connect_image_event('button_press_event', self.on_mouse_press) + self.connect_image_event('button_release_event', self.on_mouse_release) + self.connect_image_event('motion_notify_event', self.on_move) + self.connect_image_event('scroll_event', self.on_scroll) + + self.image_viewer.redraw() + + def help(self): + helpstr = ("Line profile tool", + "+ and - keys or mouse scroll changes width of scan line.", + "Select and drag ends of the scan line to adjust it.") + return '\n'.join(helpstr) + + def get_profile(self): + """Return intensity profile of the selected line. + + Returns + ------- + end_pts: (2, 2) array + The positions ((x1, y1), (x2, y2)) of the line ends. + profile: 1d array + Profile of intensity values. + """ + end_pts = self.scan_line.get_xydata() + profile = self.profile.get_ydata() + return end_pts, profile + + def on_scroll(self, event): + if not event.inaxes: return + if event.button == 'up': + self._thicken_scan_line() + elif event.button == 'down': + self._shrink_scan_line() + + def on_key_press(self, event): + if not event.inaxes: return + elif event.key == '+': + self._thicken_scan_line() + elif event.key == '-': + self._shrink_scan_line() + elif event.key == 'r': + self.reset() + + def _thicken_scan_line(self): + self.linewidth += 1 + self.line_changed(None, None) + + def _shrink_scan_line(self): + if self.linewidth > 1: + self.linewidth -= 1 + self.line_changed(None, None) + + def _autoscale_view(self): + if self.limits is None: + self.ax.autoscale_view(tight=True) + else: + self.ax.autoscale_view(scaley=False, tight=True) + + def get_pt_under_cursor(self, event): + """Return index of the end point under cursor, if sufficiently close""" + xy = np.asarray(self.scan_line.get_xydata()) + xyt = self.scan_line.get_transform().transform(xy) + xt, yt = xyt[:, 0], xyt[:, 1] + d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2) + indseq = np.nonzero(np.equal(d, np.amin(d)))[0] + ind = indseq[0] + if d[ind] >= self.epsilon: + ind = None + return ind + + def on_mouse_press(self, event): + if event.button != 1: return + if event.inaxes==None: return + self._active_pt = self.get_pt_under_cursor(event) + + def on_mouse_release(self, event): + if event.button != 1: return + self._active_pt = None + + def on_move(self, event): + if event.button != 1: return + if self._active_pt is None: return + if not self.image_viewer.ax.in_axes(event): return + x,y = event.xdata, event.ydata + self.line_changed(x, y) + + def reset(self): + self.end_pts = self._init_end_pts.copy() + self.scan_line.set_data(np.transpose(self.end_pts)) + self.line_changed(None, None) + + def line_changed(self, x, y): + if x is not None: + self.end_pts[self._active_pt, :] = x, y + self.scan_line.set_data(np.transpose(self.end_pts)) + self.scan_line.set_linewidth(self.linewidth) + + scan = profile_line(self.image_viewer.original_image, self.end_pts, + linewidth=self.linewidth) + self.profile.set_xdata(np.arange(scan.shape[0])) + self.profile.set_ydata(scan) + + self.ax.relim() + + if self.useblit: + self.image_viewer.canvas.restore_region(self.img_background) + self.ax.draw_artist(self.scan_line) + self.ax.draw_artist(self.profile) + self.image_viewer.canvas.blit(self.image_viewer.ax.bbox) + + self._autoscale_view() + + self.image_viewer.redraw() + self.redraw() + + +def profile_line(img, end_pts, linewidth=1): + """Return the intensity profile of an image measured along a scan line. + + Parameters + ---------- + img : 2d array + The image. + end_pts: (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_pts + x1, y1 = point1 = np.asarray(point1, dtype = float) + x2, y2 = point2 = np.asarray(point2, dtype = float) + dx, dy = point2 - point1 + + # Quick calculation if perfectly horizontal or vertical (remove?) + if x1 == x2: + pixels = img[min(y1, y2) : max(y1, y2)+1, + x1 - linewidth / 2 : x1 + linewidth / 2 + 1] + intensities = pixels.mean(axis = 1) + return intensities + elif y1 == y2: + pixels = img[y1 - linewidth / 2 : y1 + linewidth / 2 + 1, + min(x1, x2) : max(x1, x2)+1] + intensities = pixels.mean(axis = 0) + return intensities + + theta = np.arctan2(dy,dx) + a = dy/dx + b = y1 - a * x1 + length = np.hypot(dx, dy) + + line_x = np.linspace(min(x1, x2), max(x1, x2), 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]) + pixels = ndi.map_coordinates(img, perp_lines) + intensities = pixels.mean(axis=1) + + return intensities diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py new file mode 100644 index 00000000..fa06088d --- /dev/null +++ b/skimage/viewer/plugins/plotplugin.py @@ -0,0 +1,50 @@ +import numpy as np +from PyQt4 import QtGui + +import matplotlib.pyplot as plt + +from ..utils import MatplotlibCanvas +from .base import Plugin + + +class PlotCanvas(MatplotlibCanvas): + """Canvas for displaying images. + + This canvas derives from Matplotlib, and has attributes `fig` and `ax`, + which point to Matplotlib figure and axes. + """ + def __init__(self, parent, height, width, **kwargs): + self.fig, self.ax = plt.subplots(figsize=(height, width), **kwargs) + super(PlotCanvas, self).__init__(parent, self.fig, **kwargs) + self.setMinimumHeight(150) + +class PlotPlugin(Plugin): + """Plugin for ImageViewer that contains a plot canvas. + + Base class for plugins that contain a Matplotlib plot canvas, which can, + for example, display an image histogram. + + See base Plugin class for additional details. + """ + + def attach(self, image_viewer): + super(PlotPlugin, self).attach(image_viewer) + # Add plot for displaying intensity profile. + self.add_plot() + + def redraw(self): + """Redraw plot.""" + self.canvas.draw_idle() + + def add_plot(self, height=4, width=4): + self.canvas = PlotCanvas(self, height, width) + self.fig = self.canvas.fig + #TODO: Converted color is slightly different than Qt background. + qpalette = QtGui.QPalette() + qcolor = qpalette.color(QtGui.QPalette.Window) + bgcolor = qcolor.toRgb().value() + if np.isscalar(bgcolor): + bgcolor = str(bgcolor / 255.) + self.fig.patch.set_facecolor(bgcolor) + self.ax = self.canvas.ax + self.layout.addWidget(self.canvas, self.row, 0) diff --git a/viewer_examples/plugins/lineprofile.py b/viewer_examples/plugins/lineprofile.py new file mode 100644 index 00000000..2f1b2cdc --- /dev/null +++ b/viewer_examples/plugins/lineprofile.py @@ -0,0 +1,9 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.lineprofile import LineProfile + + +image = data.camera() +viewer = ImageViewer(image) +viewer += LineProfile() +viewer.show() From 224fcb5d01ffd12a1048da361e4c3a1951364320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 2 Sep 2012 09:58:02 +0200 Subject: [PATCH 439/648] Convert selem to uint8 --- skimage/morphology/grey.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 7ce25475..00db0fae 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -66,6 +66,7 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): if image is out: raise NotImplementedError("In-place erosion not supported!") image = skimage.img_as_ubyte(image) + selem = skimage.img_as_ubyte(selem) return cmorph.erode(image, selem, out=out, shift_x=shift_x, shift_y=shift_y) @@ -119,6 +120,7 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): if image is out: raise NotImplementedError("In-place dilation not supported!") image = skimage.img_as_ubyte(image) + selem = skimage.img_as_ubyte(selem) return cmorph.dilate(image, selem, out=out, shift_x=shift_x, shift_y=shift_y) From 2842515dc3f47f228e1a0548b02cdfdec01eaa43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 2 Sep 2012 10:34:09 +0200 Subject: [PATCH 440/648] Improve doc string layout --- skimage/morphology/grey.py | 77 +++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 00db0fae..5ef6b9af 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -28,15 +28,12 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array. - + Image array. selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - + The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None is - passed, a new array will be allocated. - + The array to store the result of the morphology. If None is + passed, a new array will be allocated. shift_x, shift_y : bool shift structuring element about center point. This only affects eccentric structuring elements (i.e. selem with even numbered sides). @@ -44,7 +41,7 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): Returns ------- eroded : uint8 array - The result of the morphological erosion. + The result of the morphological erosion. Examples -------- @@ -63,6 +60,7 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): [0, 0, 0, 0, 0]], dtype='uint8') """ + if image is out: raise NotImplementedError("In-place erosion not supported!") image = skimage.img_as_ubyte(image) @@ -82,15 +80,12 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): ---------- image : ndarray - Image array. - + Image array. selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - + The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None, is - passed, a new array will be allocated. - + The array to store the result of the morphology. If None, is + passed, a new array will be allocated. shift_x, shift_y : bool shift structuring element about center point. This only affects eccentric structuring elements (i.e. selem with even numbered sides). @@ -98,7 +93,7 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): Returns ------- dilated : uint8 array - The result of the morphological dilation. + The result of the morphological dilation. Examples -------- @@ -117,6 +112,7 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): [0, 0, 0, 0, 0]], dtype='uint8') """ + if image is out: raise NotImplementedError("In-place dilation not supported!") image = skimage.img_as_ubyte(image) @@ -136,19 +132,17 @@ def opening(image, selem, out=None): Parameters ---------- image : ndarray - Image array. - + Image array. selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - + The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None - is passed, a new array will be allocated. + The array to store the result of the morphology. If None + is passed, a new array will be allocated. Returns ------- opening : uint8 array - The result of the morphological opening. + The result of the morphological opening. Examples -------- @@ -167,6 +161,7 @@ def opening(image, selem, out=None): [0, 0, 0, 0, 0]], dtype='uint8') """ + h, w = selem.shape shift_x = True if (w % 2) == 0 else False shift_y = True if (h % 2) == 0 else False @@ -187,19 +182,17 @@ def closing(image, selem, out=None): Parameters ---------- image : ndarray - Image array. - + Image array. selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - + The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None, - is passed, a new array will be allocated. + The array to store the result of the morphology. If None, + is passed, a new array will be allocated. Returns ------- closing : uint8 array - The result of the morphological closing. + The result of the morphological closing. Examples -------- @@ -218,6 +211,7 @@ def closing(image, selem, out=None): [0, 0, 0, 0, 0]], dtype='uint8') """ + h, w = selem.shape shift_x = True if (w % 2) == 0 else False shift_y = True if (h % 2) == 0 else False @@ -237,19 +231,17 @@ def white_tophat(image, selem, out=None): Parameters ---------- image : ndarray - Image array. - + Image array. selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - + The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None - is passed, a new array will be allocated. + The array to store the result of the morphology. If None + is passed, a new array will be allocated. Returns ------- opening : uint8 array - The result of the morphological white top hat. + The result of the morphological white top hat. Examples -------- @@ -288,14 +280,12 @@ def black_tophat(image, selem, out=None): Parameters ---------- image : ndarray - Image array. - + Image array. selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - + The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None - is passed, a new array will be allocated. + The array to store the result of the morphology. If None + is passed, a new array will be allocated. Returns ------- @@ -319,6 +309,7 @@ def black_tophat(image, selem, out=None): [0, 0, 0, 0, 0]], dtype='uint8') """ + if image is out: raise NotImplementedError("Cannot perform white top hat in place.") image = skimage.img_as_ubyte(image) From de47332bd2882edc7325830888e753d068633157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 2 Sep 2012 13:25:52 +0200 Subject: [PATCH 441/648] Add fast morphological operations for binary images --- skimage/morphology/__init__.py | 2 + skimage/morphology/binary.py | 133 ++++++++++++++++++++++++++ skimage/morphology/tests/test_grey.py | 32 ++++++- 3 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 skimage/morphology/binary.py diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index efa2077d..d4c775eb 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -1,3 +1,5 @@ +from .binary import (binary_erosion, binary_dilation, binary_opening, + binary_closing) from .grey import * from .selem import * from .ccomp import label diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py new file mode 100644 index 00000000..768472d3 --- /dev/null +++ b/skimage/morphology/binary.py @@ -0,0 +1,133 @@ +import numpy as np +from scipy import ndimage + + +def binary_erosion(image, selem, out=None): + """Return fast binary morphological erosion of an image. + + This function returns the same result as greyscale erosion but performs + faster for binary images. + + Morphological erosion sets a pixel at (i,j) to the minimum over all pixels + in the neighborhood centered at (i,j). Erosion shrinks bright regions and + enlarges dark regions. + + Parameters + ---------- + image : ndarray + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + The array to store the result of the morphology. If None is + passed, a new array will be allocated. + + Returns + ------- + eroded : bool array + The result of the morphological erosion. + + """ + + conv = ndimage.convolve(image > 0, selem, output=out, + mode='constant', cval=1) + return conv == np.sum(selem) + + +def binary_dilation(image, selem, out=None): + """Return fast binary morphological dilation of an image. + + This function returns the same result as greyscale dilation but performs + faster for binary images. + + Morphological dilation sets a pixel at (i,j) to the maximum over all pixels + in the neighborhood centered at (i,j). Dilation enlarges bright regions + and shrinks dark regions. + + Parameters + ---------- + + image : ndarray + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + The array to store the result of the morphology. If None, is + passed, a new array will be allocated. + + Returns + ------- + dilated : bool array + The result of the morphological dilation. + + """ + + conv = ndimage.convolve(image > 0, selem, output=out, + mode='constant', cval=1) + return conv != 0 + + +def binary_opening(image, selem, out=None): + """Return fast binary morphological opening of an image. + + This function returns the same result as greyscale opening but performs + faster for binary images. + + The morphological opening on an image is defined as an erosion followed by + a dilation. Opening can remove small bright spots (i.e. "salt") and connect + small dark cracks. This tends to "open" up (dark) gaps between (bright) + features. + + Parameters + ---------- + image : ndarray + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + The array to store the result of the morphology. If None + is passed, a new array will be allocated. + + Returns + ------- + opening : bool array + The result of the morphological opening. + + """ + + eroded = binary_erosion(image, selem) + out = binary_dilation(eroded, selem, out=out) + return out + + +def binary_closing(image, selem, out=None): + """Return fast binary morphological closing of an image. + + This function returns the same result as greyscale closing but performs + faster for binary images. + + The morphological closing on an image is defined as a dilation followed by + an erosion. Closing can remove small dark spots (i.e. "pepper") and connect + small bright cracks. This tends to "close" up (dark) gaps between (bright) + features. + + Parameters + ---------- + image : ndarray + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + The array to store the result of the morphology. If None, + is passed, a new array will be allocated. + + Returns + ------- + closing : bool array + The result of the morphological closing. + + """ + + dilated = binary_dilation(image, selem) + out = binary_erosion(dilated, selem, out=out) + return out diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index d7242e8f..5d5b6c53 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -5,11 +5,11 @@ from numpy import testing import skimage from skimage import data_dir -from skimage.morphology import grey -from skimage.morphology import selem +from skimage.morphology import binary, grey, selem lena = np.load(os.path.join(data_dir, 'lena_GRAY_U8.npy')) +bw_lena = lena > 0.4 class TestMorphology(): @@ -154,5 +154,33 @@ class TestDTypes(): self._test_image(image) +def test_binary_erosion(): + strel = selem.square(3) + binary_res = binary.binary_erosion(bw_lena, strel) + grey_res = grey.erosion(bw_lena, strel) + assert np.all(binary_res == grey_res) + + +def test_binary_dilation(): + strel = selem.square(3) + binary_res = binary.binary_dilation(bw_lena, strel) + grey_res = grey.dilation(bw_lena, strel) + assert np.all(binary_res == grey_res) + + +def test_binary_closing(): + strel = selem.square(3) + binary_res = binary.binary_closing(bw_lena, strel) + grey_res = grey.closing(bw_lena, strel) + assert np.all(binary_res == grey_res) + + +def test_binary_opening(): + strel = selem.square(3) + binary_res = binary.binary_opening(bw_lena, strel) + grey_res = grey.opening(bw_lena, strel) + assert np.all(binary_res == grey_res) + + if __name__ == '__main__': testing.run_module_suite() From 373b3293eecf51f3e638aed022c4f33d62d88823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 2 Sep 2012 13:28:07 +0200 Subject: [PATCH 442/648] Use numpy testing functions --- skimage/morphology/tests/test_grey.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 5d5b6c53..6003425f 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -158,28 +158,28 @@ def test_binary_erosion(): strel = selem.square(3) binary_res = binary.binary_erosion(bw_lena, strel) grey_res = grey.erosion(bw_lena, strel) - assert np.all(binary_res == grey_res) + testing.assert_array_equal(binary_res, grey_res) def test_binary_dilation(): strel = selem.square(3) binary_res = binary.binary_dilation(bw_lena, strel) grey_res = grey.dilation(bw_lena, strel) - assert np.all(binary_res == grey_res) + testing.assert_array_equal(binary_res, grey_res) def test_binary_closing(): strel = selem.square(3) binary_res = binary.binary_closing(bw_lena, strel) grey_res = grey.closing(bw_lena, strel) - assert np.all(binary_res == grey_res) + testing.assert_array_equal(binary_res, grey_res) def test_binary_opening(): strel = selem.square(3) binary_res = binary.binary_opening(bw_lena, strel) grey_res = grey.opening(bw_lena, strel) - assert np.all(binary_res == grey_res) + testing.assert_array_equal(binary_res, grey_res) if __name__ == '__main__': From 2c37c70ca36705cc7d2a35cf1cd819e8161b5363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 2 Sep 2012 13:35:12 +0200 Subject: [PATCH 443/648] Fix support for predefined output array --- skimage/morphology/binary.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 768472d3..eef31a3d 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -29,9 +29,9 @@ def binary_erosion(image, selem, out=None): """ - conv = ndimage.convolve(image > 0, selem, output=out, - mode='constant', cval=1) - return conv == np.sum(selem) + out = ndimage.convolve(image > 0, selem, output=out, + mode='constant', cval=1) + return np.equal(out, np.sum(selem), out=out) def binary_dilation(image, selem, out=None): @@ -62,9 +62,9 @@ def binary_dilation(image, selem, out=None): """ - conv = ndimage.convolve(image > 0, selem, output=out, - mode='constant', cval=1) - return conv != 0 + out = ndimage.convolve(image > 0, selem, output=out, + mode='constant', cval=1) + return np.not_equal(out, 0, out=out) def binary_opening(image, selem, out=None): From aa08e8a559113cdb281d045a83ed400d00a86953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 2 Sep 2012 13:41:25 +0200 Subject: [PATCH 444/648] Fix for predefined output array --- skimage/morphology/binary.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index eef31a3d..ffd79482 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -29,8 +29,10 @@ def binary_erosion(image, selem, out=None): """ - out = ndimage.convolve(image > 0, selem, output=out, - mode='constant', cval=1) + conv = ndimage.convolve(image > 0, selem, output=out, + mode='constant', cval=1) + if conv is not None: + out = conv return np.equal(out, np.sum(selem), out=out) @@ -62,8 +64,10 @@ def binary_dilation(image, selem, out=None): """ - out = ndimage.convolve(image > 0, selem, output=out, - mode='constant', cval=1) + conv = ndimage.convolve(image > 0, selem, output=out, + mode='constant', cval=1) + if conv is not None: + out = conv return np.not_equal(out, 0, out=out) From 0db48936804e78b5a3f63658955a6ade395ca502 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 08:22:21 -0400 Subject: [PATCH 445/648] DOC: Fix broken doc build due to broken import --- doc/examples/plot_skeleton.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_skeleton.py b/doc/examples/plot_skeleton.py index a0ad692f..65a0ee48 100644 --- a/doc/examples/plot_skeleton.py +++ b/doc/examples/plot_skeleton.py @@ -16,7 +16,7 @@ In the case of boolean, 'True' indicates foreground, and for integer arrays, the foreground is 1's. """ from skimage.morphology import skeletonize -from skimage.draw import draw +from skimage import draw import numpy as np import matplotlib.pyplot as plt From bd0d47a1830fa23726b60e9295b94fd09d3a8417 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 12:23:17 -0400 Subject: [PATCH 446/648] DOC: Turn off numpydoc flag to silence warnings --- doc/source/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/conf.py b/doc/source/conf.py index 0bc673e6..438aaaf8 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -224,6 +224,8 @@ latex_documents = [ # Make numpydoc to generate plots for example sections #numpydoc_use_plots = True +numpydoc_show_class_members = False + # ----------------------------------------------------------------------------- # Plots # ----------------------------------------------------------------------------- From 8b15656febe96f77276a8ce1b067ec567554228e Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 12:23:52 -0400 Subject: [PATCH 447/648] Change import to silence import errors in docs. `import skimage` in submodules seems to cause issues with Sphinx autodocs. (Maybe some sort of circular import issue.) Note the `ImportErrors` fixed by this commit don't actually cause sphinx build errors; Sphinx seems to capture the errors, but it's annoyingly noisy, nonetheless. --- skimage/exposure/exposure.py | 4 ++-- skimage/morphology/grey.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index b0ef45cf..bffe660a 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -1,6 +1,6 @@ import numpy as np -import skimage +from skimage import img_as_float from skimage.util.dtype import dtype_range @@ -101,7 +101,7 @@ def equalize(image, nbins=256): .. [2] http://en.wikipedia.org/wiki/Histogram_equalization """ - image = skimage.img_as_float(image) + image = img_as_float(image) cdf, bin_centers = cumulative_distribution(image, nbins) out = np.interp(image.flat, bin_centers, cdf) return out.reshape(image.shape) diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 5ef6b9af..dc34d3d6 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en' import warnings import numpy as np -import skimage +from skimage import img_as_ubyte from . import cmorph @@ -63,8 +63,8 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): if image is out: raise NotImplementedError("In-place erosion not supported!") - image = skimage.img_as_ubyte(image) - selem = skimage.img_as_ubyte(selem) + image = img_as_ubyte(image) + selem = img_as_ubyte(selem) return cmorph.erode(image, selem, out=out, shift_x=shift_x, shift_y=shift_y) @@ -115,8 +115,8 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): if image is out: raise NotImplementedError("In-place dilation not supported!") - image = skimage.img_as_ubyte(image) - selem = skimage.img_as_ubyte(selem) + image = img_as_ubyte(image) + selem = img_as_ubyte(selem) return cmorph.dilate(image, selem, out=out, shift_x=shift_x, shift_y=shift_y) @@ -262,7 +262,7 @@ def white_tophat(image, selem, out=None): """ if image is out: raise NotImplementedError("Cannot perform white top hat in place.") - image = skimage.img_as_ubyte(image) + image = img_as_ubyte(image) out = opening(image, selem, out=out) out = image - out @@ -312,7 +312,7 @@ def black_tophat(image, selem, out=None): if image is out: raise NotImplementedError("Cannot perform white top hat in place.") - image = skimage.img_as_ubyte(image) + image = img_as_ubyte(image) out = closing(image, selem, out=out) out = out - image From 12c35908c90a2fe80ffa8f2e34f020ffa65aa966 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 12:32:14 -0400 Subject: [PATCH 448/648] DOC: Separate config sections in conf.py --- doc/source/conf.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 438aaaf8..cb22e6f0 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -221,9 +221,6 @@ latex_documents = [ # ----------------------------------------------------------------------------- # Numpy extensions # ----------------------------------------------------------------------------- -# Make numpydoc to generate plots for example sections -#numpydoc_use_plots = True - numpydoc_show_class_members = False # ----------------------------------------------------------------------------- @@ -259,7 +256,9 @@ plot2rst_index_name = 'README' plot2rst_rcparams = {'image.cmap' : 'gray', 'image.interpolation' : 'none'} - +# ----------------------------------------------------------------------------- +# intersphinx +# ----------------------------------------------------------------------------- _python_doc_base = 'http://docs.python.org/2.7' intersphinx_mapping = { _python_doc_base: None, From a9116f877e921b65f731790d0c76642cea557319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 2 Sep 2012 20:15:44 +0200 Subject: [PATCH 449/648] Hide coefficient variable of geometric transforms --- skimage/transform/_geometric.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 44bc8651..3e469ba7 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -80,7 +80,7 @@ class ProjectiveTransform(GeometricTransform): """ - coeffs = range(8) + _coeffs = range(8) def __init__(self, matrix=None): if matrix is None: @@ -187,14 +187,14 @@ class ProjectiveTransform(GeometricTransform): A[rows:, 8] = yd # Select relevant columns, depending on params - A = A[:, self.coeffs + [8]] + A = A[:, self._coeffs + [8]] _, _, V = np.linalg.svd(A) H = np.zeros((3, 3)) # solution is right singular vector that corresponds to smallest # singular value - H.flat[self.coeffs + [8]] = - V[-1, :-1] / V[-1, -1] + H.flat[self._coeffs + [8]] = - V[-1, :-1] / V[-1, -1] H[2, 2] = 1 self._matrix = H @@ -248,7 +248,7 @@ class AffineTransform(ProjectiveTransform): """ - coeffs = range(6) + _coeffs = range(6) def __init__(self, matrix=None, scale=None, rotation=None, shear=None, translation=None): From 12b8d8d051587006a8fee0a690df60c2c106fedf Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Sun, 2 Sep 2012 21:17:18 +0200 Subject: [PATCH 450/648] cleanup of tests hence all results of tv denoise operations are returned as float --- skimage/filter/tests/test_tv_denoise.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/skimage/filter/tests/test_tv_denoise.py b/skimage/filter/tests/test_tv_denoise.py index afee6b02..635dfdcd 100644 --- a/skimage/filter/tests/test_tv_denoise.py +++ b/skimage/filter/tests/test_tv_denoise.py @@ -27,11 +27,9 @@ class TestTvDenoise(): grad_denoised = ndimage.morphological_gradient( denoised_lena, size=((3, 3))) # test if the total variation has decreased + assert grad_denoised.dtype == np.float assert (np.sqrt((grad_denoised**2).sum()) < np.sqrt((grad**2).sum()) / 2) - denoised_lena_int = img_as_uint(filter.tv_denoise(img_as_ubyte(lena), - weight=60.0)) - assert denoised_lena_int.dtype is np.dtype('uint16') def test_tv_denoise_float_result_range(self): # lena image @@ -40,6 +38,7 @@ class TestTvDenoise(): assert np.max(int_lena) > 1 denoised_int_lena = filter.tv_denoise(int_lena, weight=60.0) # test if the value range of output float data is within [0.0:1.0] + assert denoised_int_lena.dtype == np.float assert np.max(denoised_int_lena) <= 1.0 assert np.min(denoised_int_lena) >= 0.0 @@ -55,12 +54,9 @@ class TestTvDenoise(): mask += 20 * np.random.randn(*mask.shape) mask[mask < 0] = 0 mask[mask > 255] = 255 - res = img_as_ubyte(filter.tv_denoise(mask.astype(np.uint8), - weight=100)) - assert res.std() < mask.std() - assert res.dtype is np.dtype('uint8') - res = img_as_ubyte(filter.tv_denoise(mask.astype(np.uint8), weight=100)) - assert res.std() < mask.std() + res = filter.tv_denoise(mask.astype(np.uint8), weight=100) + assert res.dtype == np.float + assert res.std() * 255 < mask.std() # test wrong number of dimensions a = np.random.random((8, 8, 8, 8)) @@ -69,5 +65,6 @@ class TestTvDenoise(): except ValueError: pass + if __name__ == "__main__": run_module_suite() From c85105308456d3d76e409566de5d84d4f6348e4b Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Sun, 2 Sep 2012 21:18:00 +0200 Subject: [PATCH 451/648] specified float array result in docstring --- skimage/filter/_tv_denoise.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/skimage/filter/_tv_denoise.py b/skimage/filter/_tv_denoise.py index 76affb98..4f663ae2 100644 --- a/skimage/filter/_tv_denoise.py +++ b/skimage/filter/_tv_denoise.py @@ -27,7 +27,7 @@ def _tv_denoise_3d(im, weight=100, eps=2.e-4, n_iter_max=200): Returns ------- out: ndarray - denoised array + denoised array of floats Notes ----- @@ -110,7 +110,7 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): Returns ------- out: ndarray - denoised array + denoised array of floats Notes ----- @@ -198,8 +198,7 @@ def tv_denoise(im, weight=50, eps=2.e-4, n_iter_max=200): Returns ------- out: ndarray - denoised array - + denoised array of floats Notes ----- From b1098f69f8706271b6dd76a4fe4afe9d3af3e801 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 2 Sep 2012 23:21:13 +0200 Subject: [PATCH 452/648] DOC: removed unused import in docstring --- skimage/segmentation/_slic.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index b6831e80..d0e4c2a3 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -48,7 +48,6 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, -------- >>> from skimage.segmentation import slic >>> from skimage.data import lena - >>> from skimage.util import img_as_float >>> img = lena() >>> segments = slic(img, n_segments=100, ratio=10) >>> # Increasing the ratio parameter yields more square regions From 0a92bf84e1dbf36cddccc48f09b52f6607186351 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 13:11:51 -0400 Subject: [PATCH 453/648] DOC: Remove inheritance-diagram Sphinx role. Either the role should be removed (as done here) or the inheritance-diagram extension needs to be added to the config file. Since skimage doesn't have a complicated class hierarchies, it's probably best to remove. --- doc/tools/apigen.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/doc/tools/apigen.py b/doc/tools/apigen.py index 86791383..0fd6c8c2 100644 --- a/doc/tools/apigen.py +++ b/doc/tools/apigen.py @@ -281,11 +281,6 @@ class ApiDocWriter(object): title = ':mod:`' + uri_short + '`' ad += title + '\n' + self.rst_section_levels[1] * len(title) - if len(classes): - ad += '\nInheritance diagram for ``%s``:\n\n' % uri - ad += '.. inheritance-diagram:: %s \n' % uri - ad += ' :parts: 3\n' - ad += '\n.. automodule:: ' + uri + '\n' ad += '\n.. currentmodule:: ' + uri + '\n' # multi_class = len(classes) > 1 From e6ca084aee670d8c7f6f4c094e78e566d6d149d0 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 13:22:58 -0400 Subject: [PATCH 454/648] DOC: Remove inherited-members Sphinx role. Documenting inherited methods and attributes can lead to overly verbose docs. For example, the `Image` class, which subclasses ndarray, and various viewer-related classes, which subclass PyQt and Matplotlib classes, have hundreds of irrelevant methods added to the API docs. --- doc/tools/apigen.py | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/tools/apigen.py b/doc/tools/apigen.py index 0fd6c8c2..7e306dd8 100644 --- a/doc/tools/apigen.py +++ b/doc/tools/apigen.py @@ -300,7 +300,6 @@ class ApiDocWriter(object): ad += ' :members:\n' \ ' :undoc-members:\n' \ ' :show-inheritance:\n' \ - ' :inherited-members:\n' \ '\n' \ ' .. automethod:: __init__\n' # if multi_fx: From 0643b8108fefb76dbb32b5d4f3ca75fc08bd42e1 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 13:27:08 -0400 Subject: [PATCH 455/648] DOC: Remove commented out code. --- doc/tools/apigen.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/doc/tools/apigen.py b/doc/tools/apigen.py index 7e306dd8..f13f2a1b 100644 --- a/doc/tools/apigen.py +++ b/doc/tools/apigen.py @@ -269,10 +269,6 @@ class ApiDocWriter(object): ad = '.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n' - chap_title = uri_short - #ad += (chap_title+'\n'+ self.rst_section_levels[1] * len(chap_title) - # + '\n\n') - # Set the chapter title to read 'module' for all modules except for the # main packages if '.' in uri: @@ -283,14 +279,6 @@ class ApiDocWriter(object): ad += '\n.. automodule:: ' + uri + '\n' ad += '\n.. currentmodule:: ' + uri + '\n' -# multi_class = len(classes) > 1 -# multi_fx = len(functions) > 1 -# if multi_class: -# ad += '\n' + 'Classes' + '\n' + \ -# self.rst_section_levels[2] * 7 + '\n' -# elif len(classes) and multi_fx: -# ad += '\n' + 'Class' + '\n' + \ -# self.rst_section_levels[2] * 5 + '\n' for c in classes: ad += '\n:class:`' + c + '`\n' \ + self.rst_section_levels[2] * \ @@ -302,12 +290,6 @@ class ApiDocWriter(object): ' :show-inheritance:\n' \ '\n' \ ' .. automethod:: __init__\n' -# if multi_fx: -# ad += '\n' + 'Functions' + '\n' + \ -# self.rst_section_levels[2] * 9 + '\n\n' -# elif len(functions) and multi_class: -# ad += '\n' + 'Function' + '\n' + \ -# self.rst_section_levels[2] * 8 + '\n\n' ad += '.. autosummary::\n\n' for f in functions: ad += ' ' + uri + '.' + f + '\n' From ec22dba25716d678403c4ac09bf565d14cb72348 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 15:46:39 -0400 Subject: [PATCH 456/648] DOC: Fix Sphinx warnings for regionprops In particular: - Indented equations in docstring for the `properties` parameter *must* be surrounded by whitespace to prevent Sphinx warnings. - Fix reference rendering --- skimage/measure/_regionprops.py | 55 +++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index dfc88b4f..ecea6023 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -64,96 +64,139 @@ def regionprops(label_image, properties=['Area', 'Centroid'], Shape measurements to be determined for each labelled image region. Default is `['Area', 'Centroid']`. The following properties can be determined: + * Area : int Number of pixels of region. + * BoundingBox : tuple Bounding box `(min_row, min_col, max_row, max_col)` + * CentralMoments : 3 x 3 ndarray Central moments (translation invariant) up to 3rd order. + mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } + where the sum is over the `x`, `y` coordinates of the region, and `x_c` and `y_c` are the coordinates of the region's centroid. + * Centroid : array Centroid coordinate tuple `(row, col)`. + * ConvexArea : int Number of pixels of convex hull image. + * ConvexImage : (H, J) ndarray Binary convex hull image which has the same size as bounding box. + * Coordinates : (N, 2) ndarray Coordinate list `(row, col)` of the region. + * Eccentricity : float Eccentricity of the ellipse that has the same second-moments as the region. The eccentricity is the ratio of the distance between its minor and major axis length. The value is between 0 and 1. + * EquivDiameter : float The diameter of a circle with the same area as the region. + * EulerNumber : int Euler number of region. Computed as number of objects (= 1) subtracted by number of holes (8-connectivity). + * Extent : float Ratio of pixels in the region to pixels in the total bounding box. Computed as `Area / (rows*cols)` + * FilledArea : int Number of pixels of filled region. + * FilledImage : (H, J) ndarray Binary region image with filled holes which has the same size as bounding box. + * HuMoments : tuple Hu moments (translation, scale and rotation invariant). + * Image : (H, J) ndarray Sliced binary region image which has the same size as bounding box. + * MajorAxisLength : float The length of the major axis of the ellipse that has the same normalized second central moments as the region. + * MaxIntensity: float Value with the greatest intensity in the region. + * MeanIntensity: float Value with the mean intensity in the region. + * MinIntensity: float Value with the least intensity in the region. + * MinorAxisLength : float The length of the minor axis of the ellipse that has the same normalized second central moments as the region. + * Moments : 3 x 3 ndarray Spatial moments up to 3rd order. + m_ji = sum{ array(x, y) * x^j * y^i } + where the sum is over the `x`, `y` coordinates of the region. + * NormalizedMoments : 3 x 3 ndarray Normalized moments (translation and scale invariant) up to 3rd order. + nu_ji = mu_ji / m_00^[(i+j)/2 + 1] + where `m_00` is the zeroth spatial moment. + * Orientation : float Angle between the X-axis and the major axis of the ellipse that has the same second-moments as the region. Ranging from `-pi/2` to `pi/2` in counter-clockwise direction. + * Perimeter : float Perimeter of object which approximates the contour as a line through the centers of border pixels using a 4-connectivity. + * Solidity : float Ratio of pixels in the region to pixels of the convex hull image. + * WeightedCentralMoments : 3 x 3 ndarray Central moments (translation invariant) of intensity image up to 3rd order. + wmu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } + where the sum is over the `x`, `y` coordinates of the region, and `x_c` and `y_c` are the coordinates of the region's centroid. + * WeightedCentroid : array Centroid coordinate tuple `(row, col)` weighted with intensity image. + * WeightedHuMoments : tuple Hu moments (translation, scale and rotation invariant) of intensity image. + * WeightedMoments : (3, 3) ndarray Spatial moments of intensity image up to 3rd order. + wm_ji = sum{ array(x, y) * x^j * y^i } + where the sum is over the `x`, `y` coordinates of the region. + * WeightedNormalizedMoments : 3 x 3 ndarray Normalized moments (translation and scale invariant) of intensity image up to 3rd order. + wnu_ji = wmu_ji / wm_00^[(i+j)/2 + 1] + where `wm_00` is the zeroth spatial moment (intensity-weighted area). + intensity_image : (N, M) ndarray, optional Intensity image with same size as labelled image. Default is None. @@ -214,11 +257,11 @@ def regionprops(label_image, properties=['Area', 'Centroid'], cc = m[1, 0] / m[0, 0] mu = _moments.central_moments(array, cr, cc, 3) - #: elements of the inertia tensor [a b; b c] + # elements of the inertia tensor [a b; b c] a = mu[2, 0] / mu[0, 0] b = mu[1, 1] / mu[0, 0] c = mu[0, 2] / mu[0, 0] - #: eigen values of inertia tensor + # eigen values of inertia tensor l1 = (a + c) / 2 + sqrt(4 * b ** 2 + (a - c) ** 2) / 2 l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 @@ -378,7 +421,7 @@ def perimeter(image, neighbourhood=4): ---------- image : array binary image - neighbourhood: 4 or 8, optional + neighbourhood : 4 or 8, optional neighbourhood connectivity for border pixel determination, default 4 Returns @@ -388,9 +431,9 @@ def perimeter(image, neighbourhood=4): References ---------- - K. Benkrid, D. Crookes. Design and FPGA Implementation of a Perimeter - Estimator. The Queen's University of Belfast. - http://www.cs.qub.ac.uk/~d.crookes/webpubs/papers/perimeter.doc + .. [1] K. Benkrid, D. Crookes. Design and FPGA Implementation of + a Perimeter Estimator. The Queen's University of Belfast. + http://www.cs.qub.ac.uk/~d.crookes/webpubs/papers/perimeter.doc """ if neighbourhood == 4: strel = STREL_4 From bfa6f05e1fa17283fbb6ed375c0ae5a4a46d4e98 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 15:49:43 -0400 Subject: [PATCH 457/648] DOC: Change title to match numpy docstring standard --- skimage/io/_io.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 122ddbd6..4ff8cc27 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -29,8 +29,8 @@ class Image(np.ndarray): def __new__(cls, arr, **kwargs): """Set the image data and tags according to given parameters. - Input: - ------ + Parameters + ---------- arr : ndarray Image data. kwargs : Image tags as keywords From 1d91ed98ff71d8948e30f193074e32fd5026601c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 15:50:32 -0400 Subject: [PATCH 458/648] DOC: Fix Sphinx warning. Trailing underscores are used to mark links. Wrap in name in backticks to prevent Sphinx for confusing this as a link. --- skimage/util/dtype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index 4a5a6f23..0ca3197b 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -344,7 +344,7 @@ def img_as_bool(image, force_copy=False): Returns ------- - out : ndarray of bool (bool_) + out : ndarray of bool (`bool_`) Output image. Notes From dc10efe4e1858c3aeb6e4e986f273b8d99371ec7 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 17:51:37 -0400 Subject: [PATCH 459/648] DOC: Fix formatting to prevent Sphinx warnings --- skimage/filter/thresholding.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 3f8acdef..77f244fc 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -24,11 +24,13 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, method : {'generic', 'gaussian', 'mean', 'median'}, optional Method used to determine adaptive threshold for local neighbourhood in weighted mean image. - * 'generic': use custom function (see `param` parameter) - * 'gaussian': apply gaussian filter (see `param` parameter for custom - sigma value) - * 'mean': apply arithmetic mean filter - * 'median' apply median rank filter + + * 'generic': use custom function (see `param` parameter) + * 'gaussian': apply gaussian filter (see `param` parameter for custom + sigma value) + * 'mean': apply arithmetic mean filter + * 'median' apply median rank filter + By default the 'gaussian' method is used. offset : float, optional Constant subtracted from weighted mean of neighborhood to calculate From 398344787fb2e368014ea6f476fe990d594c6dda Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 17:52:36 -0400 Subject: [PATCH 460/648] DOC: Reformat shape argument --- skimage/measure/_regionprops.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index ecea6023..5615ecc5 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -58,7 +58,7 @@ def regionprops(label_image, properties=['Area', 'Centroid'], Parameters ---------- - label_image : N x M ndarray + label_image : (N, M) ndarray Labelled input image. properties : {'all', list} Shape measurements to be determined for each labelled image region. @@ -71,7 +71,7 @@ def regionprops(label_image, properties=['Area', 'Centroid'], * BoundingBox : tuple Bounding box `(min_row, min_col, max_row, max_col)` - * CentralMoments : 3 x 3 ndarray + * CentralMoments : (3, 3) ndarray Central moments (translation invariant) up to 3rd order. mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } @@ -137,14 +137,14 @@ def regionprops(label_image, properties=['Area', 'Centroid'], The length of the minor axis of the ellipse that has the same normalized second central moments as the region. - * Moments : 3 x 3 ndarray + * Moments : (3, 3) ndarray Spatial moments up to 3rd order. m_ji = sum{ array(x, y) * x^j * y^i } where the sum is over the `x`, `y` coordinates of the region. - * NormalizedMoments : 3 x 3 ndarray + * NormalizedMoments : (3, 3) ndarray Normalized moments (translation and scale invariant) up to 3rd order. @@ -164,7 +164,7 @@ def regionprops(label_image, properties=['Area', 'Centroid'], * Solidity : float Ratio of pixels in the region to pixels of the convex hull image. - * WeightedCentralMoments : 3 x 3 ndarray + * WeightedCentralMoments : (3, 3) ndarray Central moments (translation invariant) of intensity image up to 3rd order. @@ -188,7 +188,7 @@ def regionprops(label_image, properties=['Area', 'Centroid'], where the sum is over the `x`, `y` coordinates of the region. - * WeightedNormalizedMoments : 3 x 3 ndarray + * WeightedNormalizedMoments : (3, 3) ndarray Normalized moments (translation and scale invariant) of intensity image up to 3rd order. From 84ac91927d69b4b094120aa5e3c04a6d2c556520 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 18:03:55 -0400 Subject: [PATCH 461/648] DOC: Fix citation syntax --- skimage/morphology/greyreconstruct.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 9e447800..1bbf33ca 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -101,17 +101,18 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): Notes ----- - The algorithm is taken from: - [1] Robinson, "Efficient morphological reconstruction: a downhill filter", - Pattern Recognition Letters 25 (2004) 1759-1767. + The algorithm is taken from [1]_. Applications for greyscale reconstruction + are discussed in [2]_ and [3]_. - Applications for greyscale reconstruction are discussed in: - - [2] Vincent, L., "Morphological Grayscale Reconstruction in Image Analysis: - Applications and Efficient Algorithms", IEEE Transactions on Image - Processing (1993) - [3] Soille, P., "Morphological Image Analysis: Principles and Applications", - Chapter 6, 2nd edition (2003), ISBN 3540429883. + References + ---------- + .. [1] Robinson, "Efficient morphological reconstruction: a downhill + filter", Pattern Recognition Letters 25 (2004) 1759-1767. + .. [2] Vincent, L., "Morphological Grayscale Reconstruction in Image + Analysis: Applications and Efficient Algorithms", IEEE Transactions + on Image Processing (1993) + .. [3] Soille, P., "Morphological Image Analysis: Principles and + Applications", Chapter 6, 2nd edition (2003), ISBN 3540429883. """ assert tuple(seed.shape) == tuple(mask.shape) if method == 'dilation' and np.any(seed > mask): From d67e81742dcf42c1218a48a5c519351c84ed1744 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 18:10:14 -0400 Subject: [PATCH 462/648] DOC: Shorten plugin description so it fits in table --- skimage/io/_plugins/tifffile_plugin.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/_plugins/tifffile_plugin.ini b/skimage/io/_plugins/tifffile_plugin.ini index 4666e503..bf83fce2 100644 --- a/skimage/io/_plugins/tifffile_plugin.ini +++ b/skimage/io/_plugins/tifffile_plugin.ini @@ -1,3 +1,3 @@ [tifffile] -description = Open and save TIFF and TIFF-based (LSM, STK, etc.) images using tifffile.py +description = Load and save TIFF and TIFF-based images using tifffile.py provides = imread, imsave From 27cac73e162161c08edfef239a7dd445bba31107 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 18:33:38 -0400 Subject: [PATCH 463/648] DOC: Fix section titles to match numpy doc standard --- skimage/graph/_mcp.pyx | 4 ++-- skimage/util/montage.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/graph/_mcp.pyx b/skimage/graph/_mcp.pyx index beb814fe..6c7e0868 100644 --- a/skimage/graph/_mcp.pyx +++ b/skimage/graph/_mcp.pyx @@ -130,8 +130,8 @@ def make_offsets(d, fully_connected): ------- offsets : list of tuples of length `d` - Example - ------- + Examples + -------- The singly-connected 2-d neighborhood is four offsets: diff --git a/skimage/util/montage.py b/skimage/util/montage.py index 6c23ccbf..1a8cda23 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -45,8 +45,8 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): Output array where 'alpha' has been determined automatically to fit (at least) the `n_images` in `arr_in`. - Example - ------- + Examples + -------- >>> import numpy as np >>> from skimage.util.montage import montage2d >>> arr_in = np.arange(3 * 2 * 2).reshape(3, 2, 2) From 52b5383b09f2b3b2bb722627d1d9ce82985da261 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 18:50:16 -0400 Subject: [PATCH 464/648] DOC: Fix rendering bug. ``**`` is interpreted by Sphinx as the start of bold text. Wrap in backticks to prevent misinterpretation. --- skimage/transform/_geometric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 3e469ba7..0f76b50e 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -787,7 +787,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, ---------- image : 2-D array Input image. - inverse_map : transformation object, callable xy = f(xy, **kwargs) + inverse_map : transformation object, callable ``xy = f(xy, **kwargs)`` Inverse coordinate map. A function that transforms a (N, 2) array of ``(x, y)`` coordinates in the *output image* into their corresponding coordinates in the *source image* (e.g. a transformation object or its From 3321e94d1df27edc00bdc47b1540ed43787b837f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 19:23:41 -0400 Subject: [PATCH 465/648] DOC: Wrap long lines --- skimage/transform/hough_transform.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index e4cb5b5c..5b36b103 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -61,8 +61,9 @@ except ImportError: pass -def probabilistic_hough(img, threshold=10, line_length=50, line_gap=10, theta=None): - """Performs a progressive probabilistic line Hough transform and returns the detected lines. +def probabilistic_hough(img, threshold=10, line_length=50, line_gap=10, + theta=None): + """Return lines from a progressive probabilistic line Hough transform. Parameters ---------- @@ -87,9 +88,9 @@ def probabilistic_hough(img, threshold=10, line_length=50, line_gap=10, theta=No References ---------- - .. [1] C. Galamhos, J. Matas and J. Kittler,"Progressive probabilistic Hough - transform for line detection", in IEEE Computer Society Conference on - Computer Vision and Pattern Recognition, 1999. + .. [1] C. Galamhos, J. Matas and J. Kittler, "Progressive probabilistic + Hough transform for line detection", in IEEE Computer Society + Conference on Computer Vision and Pattern Recognition, 1999. """ return _probabilistic_hough(img, threshold, line_length, line_gap, theta) From 2ce8d897ab424949c7d1b0c10ecf33fffe6b8f54 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 19:27:51 -0400 Subject: [PATCH 466/648] DOC: Integrate uncompleted Scipy2012 Sprint tasks --- TASKS.txt | 69 +++++++++---------------------------------------------- 1 file changed, 11 insertions(+), 58 deletions(-) diff --git a/TASKS.txt b/TASKS.txt index a20cbddb..da715573 100644 --- a/TASKS.txt +++ b/TASKS.txt @@ -1,64 +1,6 @@ .. role:: strike -SciPy 2012 Sprint -================= - -Welcome! Stefan van der Walt and Tony Yu are organizing a coding sprint for -scikits-image at SciPy 2012. Anyone who's interested can join the party on July -20th starting at 9 AM (location to be determined). - -We have a list of tasks for all levels of programmers, but we'd be really -interested in new ideas as well. - -Basic ------ - -These tasks should just require some basic Python knowledge. - -Code review -``````````` - -* geometric transformation PR -* morphological reconstruction PR -* Testing visualization tools - -Docs -```` - -* Task-based examples (`Where's Waldo`_, `Flatten Sudoku Puzzle`_) -* Organize/add-topics to user guide (Add overview of packages) - -.. _Flatten Sudoku Puzzle: http://stackoverflow.com/questions/10196198/how-to-remove-convexity-defects-in-sudoku-square/11366549#11366549 -.. _Where's Waldo: http://stackoverflow.com/questions/8479058/how-do-i-find-waldo-with-mathematica - -Features -```````` - -* Add text, anti-aliasing to the draw module -* Lab color space conversion -* Add slicing to ImageCollection object -* Add imread_collection to all imread backends -* Resurrect the `Image object `__ and add - EXIF and TIFF tags. - - Add IPython display protocol. - - Add an htmlrepr to the Image object. Should we return image objects from - all I/O routines? -* Add @greyimage decorator to check if input is a greyscale image - -Intermediate ------------- - -These tasks may require some understanding of image processing algorithms or -scikits-image internals. - -* Add binary features (BRIEF, BRISK, FREAK) -* Add `STAR features `__ -* Using the visualization tools, add an FFT-domain image editor -* `Blurring kernel estimation `__ -* Better video loading (move to plugin framework, add backends) - - .. _howto_contribute: How to contribute to ``skimage`` @@ -102,14 +44,19 @@ Implement Algorithms - Convex hulls of objects in a labels matrix (simply adapt current convex hull image code--this one's low hanging fruit). Generalise this solution to also skeletonize objects in a labels matrix. +- Add binary features (BRIEF, BRISK, FREAK) +- Add `STAR features `__ +- `Blurring kernel estimation `__ Drawing (directly on an ndarray) ```````````````````````````````` - Wu's algorithm for circles - Text rendering +- Add anti-aliasing Infrastructure -------------- +- Add @greyimage decorator to check if input is a greyscale image - :strike:`Implement a new backend system so that we may start including PyOpenCL-based algorithms` @@ -175,6 +122,12 @@ io * Update ``qt_plugin.py`` and other plugins to view collections. * Rewrite GTK backend using GObject Introspection for Py3K compatibility. * Add DICOM plugin for `GDCM `__. +* Add ``imread_collection`` to all ``imread`` backends +* Better video loading (move to plugin framework, add backends) + +viewer +`````` +* Using the visualization tools, add an FFT-domain image editor docs ```` From 427a79fc6a529d6cc7750c06e7f445a1afbcf692 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 2 Sep 2012 19:32:45 -0400 Subject: [PATCH 467/648] STY: Clean up whitespace --- skimage/graph/_mcp.pyx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/graph/_mcp.pyx b/skimage/graph/_mcp.pyx index 6c7e0868..320493c2 100644 --- a/skimage/graph/_mcp.pyx +++ b/skimage/graph/_mcp.pyx @@ -102,7 +102,7 @@ def _offset_edge_map(shape, offsets): """ 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 neg_edges = -1 - indices @@ -112,7 +112,7 @@ def _offset_edge_map(shape, offsets): mins = offsets.min(axis=0) for pos, neg, mx, mn in zip(pos_edges, neg_edges, maxes, mins): pos[pos > mx] = 0 - neg[neg < mn] = 0 + neg[neg < mn] = 0 return pos_edges.astype(EDGE_D), neg_edges.astype(EDGE_D) def make_offsets(d, fully_connected): @@ -216,7 +216,7 @@ cdef class MCP: `costs` array at each point on the path. The class MCP_Geometric, on the other hand, accounts for the fact that diagonal vs. axial moves are of different lengths, and weights the path cost accordingly. - + Array elements with infinite or negative costs will simply be ignored, as will paths whose cumulative cost overflows to infinite. @@ -295,7 +295,7 @@ cdef class MCP: pos, neg = _offset_edge_map(costs.shape, self.offsets) self.flat_pos_edge_map = pos.reshape((self.dim, size), order='F') self.flat_neg_edge_map = neg.reshape((self.dim, size), order='F') - + # The offset lengths are the distances traveled along each offset self.offset_lengths = np.sqrt( @@ -449,7 +449,7 @@ cdef class MCP: # edge along any axis is_at_edge = 0 for d in range(dim): - if (flat_pos_edge_map[d, index] != 0 or + if (flat_pos_edge_map[d, index] != 0 or flat_neg_edge_map[d, index] != 0): is_at_edge = 1 break @@ -490,7 +490,7 @@ cdef class MCP: new_cost = flat_costs[new_index] if new_cost < 0 or new_cost == inf: continue - + # 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. From f2246027fd350b676c18ac99955bcbaa1b0f3f9a Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 3 Sep 2012 07:55:05 -0400 Subject: [PATCH 468/648] STY: Clean up imports --- skimage/filter/tests/test_edges.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/skimage/filter/tests/test_edges.py b/skimage/filter/tests/test_edges.py index bd9a2702..44791728 100644 --- a/skimage/filter/tests/test_edges.py +++ b/skimage/filter/tests/test_edges.py @@ -1,11 +1,6 @@ -import os - -from numpy.testing import * import numpy as np -from scipy.ndimage import binary_dilation, binary_erosion import skimage.filter as F -from skimage import data_dir, img_as_float class TestSobel(): @@ -208,4 +203,5 @@ class TestVPrewitt(): if __name__ == "__main__": - run_module_suite() + from numpy import testing + testing.run_module_suite() From 92f86432187f8f92b81a2af9ac9fa017bbfbaaf4 Mon Sep 17 00:00:00 2001 From: Tim Sheerman-Chase Date: Fri, 31 Aug 2012 18:40:17 +0100 Subject: [PATCH 469/648] Initial work to introduce piecewise affine transform --- skimage/transform/__init__.py | 5 ++- skimage/transform/_geometric.py | 73 ++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 0907544b..317d8e3d 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -4,5 +4,6 @@ from .finite_radon_transform import * from .integral import * from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, - ProjectiveTransform, PolynomialTransform) -from ._warps import resize, rotate, swirl, homography + ProjectiveTransform, PolynomialTransform, + PiecewiseAffineTransform) +from ._warps import swirl, homography diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 0f76b50e..0aa3d26f 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1,6 +1,6 @@ import math import numpy as np -from scipy import ndimage +from scipy import ndimage, spatial from skimage.util import img_as_float from ._warps_cy import _warp_fast @@ -580,6 +580,76 @@ class PolynomialTransform(GeometricTransform): 'parameters by exchanging source and destination coordinates,' 'then apply the forward transformation.') +class PiecewiseAffineTransform(ProjectiveTransform): + + """2D piecewise affine transformation. + + Parameters + ---------- + TODO + + """ + + def __init__(self): + pass + + def estimate(self, src, dst): + + #Convert input to correct types + dstPoints = np.array(dst) + srcPoints = np.array(src) + + #Split input shape into mesh + self.tess = spatial.Delaunay(srcPoints) + + #Calculate ROI in source control points + xmin, xmax = srcPoints[:,0].min(), srcPoints[:,0].max() + ymin, ymax = srcPoints[:,1].min(), srcPoints[:,1].max() + + #Find affine mapping from input positions to mean shape + self.triAffines = [] + for tri in self.tess.vertices: + srcTri = np.hstack((srcPoints[tri,:], np.ones((3,1)))).transpose() + dstTri = np.hstack((dstPoints[tri,:], np.ones((3,1)))).transpose() + + affine = AffineTransform() + affine.estimate(srcTri, dstTri) + self.triAffines.append(affine) + + def __call__(self, coords): + """Apply forward transformation. + + Parameters + ---------- + coords : (N, 2) array + source coordinates + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + + out = np.ones((coords.shape[0], 2)) * -1 + + for ptNum, pt in enumerate(coords): + #Determine which triangle contains the point + simplexIndex = self.tess.find_simplex(pt) + + if simplexIndex == -1: + #This point is outside the hull of the control points + out[ptNum,0] = 0 + out[ptNum,1] = 0 + continue + + #Calculate position in the input image + affine = self.triAffines[simplexIndex] + destPos = affine(pt) + out[ptNum,0] = destPos[0][0] + out[ptNum,1] = destPos[0][1] + + return out TRANSFORMS = { 'similarity': SimilarityTransform, @@ -593,7 +663,6 @@ HOMOGRAPHY_TRANSFORMS = ( ProjectiveTransform ) - def estimate_transform(ttype, src, dst, **kwargs): """Estimate 2D geometric transformation parameters. From be1143046bb7589fb8f61b1a8a5f2ba5fcf1745b Mon Sep 17 00:00:00 2001 From: Tim Sheerman-Chase Date: Fri, 31 Aug 2012 19:06:26 +0100 Subject: [PATCH 470/648] Achieved functional piecewise affine transform --- skimage/transform/_geometric.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 0aa3d26f..8321ba1e 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -591,29 +591,19 @@ class PiecewiseAffineTransform(ProjectiveTransform): """ def __init__(self): - pass + self.tess = None + self.triAffines = [] def estimate(self, src, dst): - #Convert input to correct types - dstPoints = np.array(dst) - srcPoints = np.array(src) - #Split input shape into mesh - self.tess = spatial.Delaunay(srcPoints) - - #Calculate ROI in source control points - xmin, xmax = srcPoints[:,0].min(), srcPoints[:,0].max() - ymin, ymax = srcPoints[:,1].min(), srcPoints[:,1].max() + self.tess = spatial.Delaunay(src) #Find affine mapping from input positions to mean shape self.triAffines = [] for tri in self.tess.vertices: - srcTri = np.hstack((srcPoints[tri,:], np.ones((3,1)))).transpose() - dstTri = np.hstack((dstPoints[tri,:], np.ones((3,1)))).transpose() - affine = AffineTransform() - affine.estimate(srcTri, dstTri) + affine.estimate(src[tri,:], dst[tri,:]) self.triAffines.append(affine) def __call__(self, coords): @@ -639,8 +629,8 @@ class PiecewiseAffineTransform(ProjectiveTransform): if simplexIndex == -1: #This point is outside the hull of the control points - out[ptNum,0] = 0 - out[ptNum,1] = 0 + out[ptNum,0] = -1 + out[ptNum,1] = -1 continue #Calculate position in the input image @@ -656,6 +646,7 @@ TRANSFORMS = { 'affine': AffineTransform, 'projective': ProjectiveTransform, 'polynomial': PolynomialTransform, + 'piecewiseaffine': PiecewiseAffineTransform, } HOMOGRAPHY_TRANSFORMS = ( SimilarityTransform, @@ -673,7 +664,7 @@ def estimate_transform(ttype, src, dst, **kwargs): Parameters ---------- - ttype : {'similarity', 'affine', 'projective', 'polynomial'} + ttype : {'similarity', 'affine', 'piecewiseaffine', 'projective', 'polynomial'} Type of transform. kwargs : array or int Function parameters (src, dst, n, angle):: @@ -681,6 +672,7 @@ def estimate_transform(ttype, src, dst, **kwargs): NAME / TTYPE FUNCTION PARAMETERS 'similarity' `src, `dst` 'affine' `src, `dst` + 'piecewiseaffine' `src, `dst` 'projective' `src, `dst` 'polynomial' `src, `dst`, `order` (polynomial order) From 07d4e333f03a89c7559e61a3c3dba92d153c31aa Mon Sep 17 00:00:00 2001 From: Tim Sheerman-Chase Date: Fri, 31 Aug 2012 19:12:57 +0100 Subject: [PATCH 471/648] Update doc strings with piecewise affine --- skimage/transform/_geometric.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 8321ba1e..8771542b 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -584,6 +584,10 @@ class PiecewiseAffineTransform(ProjectiveTransform): """2D piecewise affine transformation. + Control points are used to define the mapping. The transform is based on + a Delaunay triangulation of the points to form a mesh. Each triangle is + used to find a local affine transform. + Parameters ---------- TODO From 5617bc563fcd1bd4d75001c3063f7c1dfc1380a4 Mon Sep 17 00:00:00 2001 From: Tim Sheerman-Chase Date: Fri, 31 Aug 2012 22:47:59 +0100 Subject: [PATCH 472/648] Improve comments --- skimage/transform/_geometric.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 8771542b..785765d6 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -600,10 +600,10 @@ class PiecewiseAffineTransform(ProjectiveTransform): def estimate(self, src, dst): - #Split input shape into mesh + #Triangulate input positions into mesh self.tess = spatial.Delaunay(src) - #Find affine mapping from input positions to mean shape + #Find affine mapping from source positions to destination self.triAffines = [] for tri in self.tess.vertices: affine = AffineTransform() @@ -637,7 +637,7 @@ class PiecewiseAffineTransform(ProjectiveTransform): out[ptNum,1] = -1 continue - #Calculate position in the input image + #Calculate affine transformed position affine = self.triAffines[simplexIndex] destPos = affine(pt) out[ptNum,0] = destPos[0][0] @@ -650,7 +650,7 @@ TRANSFORMS = { 'affine': AffineTransform, 'projective': ProjectiveTransform, 'polynomial': PolynomialTransform, - 'piecewiseaffine': PiecewiseAffineTransform, + 'piecewiseaffine': PiecewiseAffineTransform, } HOMOGRAPHY_TRANSFORMS = ( SimilarityTransform, From 839442aad2e7efdfcb8766a86eaf570035eab8e0 Mon Sep 17 00:00:00 2001 From: Tim Sheerman-Chase Date: Fri, 31 Aug 2012 22:53:39 +0100 Subject: [PATCH 473/648] Improve comments --- skimage/transform/_geometric.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 785765d6..9869fd87 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -599,6 +599,18 @@ class PiecewiseAffineTransform(ProjectiveTransform): self.triAffines = [] def estimate(self, src, dst): + """Set the control points with which to perform the piecewise affine mapping. + + Number of source and destination coordinates must match. + + Parameters + ---------- + src : (N, 2) array + Source coordinates. + dst : (N, 2) array + Destination coordinates. + + """ #Triangulate input positions into mesh self.tess = spatial.Delaunay(src) From ea5fe5ff9cf941ea13fdf4f5d7578fea64d6a335 Mon Sep 17 00:00:00 2001 From: Tim Sheerman-Chase Date: Sat, 1 Sep 2012 12:38:25 +0100 Subject: [PATCH 474/648] Setting _matrix to None, because the transform is not of this form --- skimage/transform/_geometric.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 9869fd87..030197da 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -597,6 +597,7 @@ class PiecewiseAffineTransform(ProjectiveTransform): def __init__(self): self.tess = None self.triAffines = [] + self._matrix = None def estimate(self, src, dst): """Set the control points with which to perform the piecewise affine mapping. From 3e3e0928d72c9add5e3b36163e75a7a564d4b65e Mon Sep 17 00:00:00 2001 From: Tim Sheerman-Chase Date: Sat, 1 Sep 2012 12:41:23 +0100 Subject: [PATCH 475/648] Wrap long line --- skimage/transform/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 317d8e3d..48a252b2 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -4,6 +4,6 @@ from .finite_radon_transform import * from .integral import * from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, - ProjectiveTransform, PolynomialTransform, + ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) from ._warps import swirl, homography From 6e3d460b3cc203c50a76d78f49bc137536422618 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 3 Sep 2012 08:03:57 -0400 Subject: [PATCH 476/648] STY: Rename tests. Test classes were unnecessary. Simplify to functions. --- skimage/filter/tests/test_edges.py | 332 ++++++++++++++--------------- 1 file changed, 163 insertions(+), 169 deletions(-) diff --git a/skimage/filter/tests/test_edges.py b/skimage/filter/tests/test_edges.py index 44791728..7fe8da4b 100644 --- a/skimage/filter/tests/test_edges.py +++ b/skimage/filter/tests/test_edges.py @@ -3,203 +3,197 @@ import numpy as np import skimage.filter as F -class TestSobel(): - def test_00_00_zeros(self): - """Sobel on an array of all zeros""" - result = F.sobel(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) +def test_sobel_zeros(): + """Sobel on an array of all zeros""" + result = F.sobel(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) - def test_00_01_mask(self): - """Sobel on a masked array should be zero""" - np.random.seed(0) - result = F.sobel(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) +def test_sobel_mask(): + """Sobel on a masked array should be zero""" + np.random.seed(0) + result = F.sobel(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) - def test_01_01_horizontal(self): - """Sobel on an edge should be a horizontal line""" - i, j = np.mgrid[-5:6, -5:6] - image = (i >= 0).astype(float) - result = F.sobel(image) - # Fudge the eroded points - i[np.abs(j) == 5] = 10000 - assert (np.all(result[i == 0] == 1)) - assert (np.all(result[np.abs(i) > 1] == 0)) +def test_sobel_horizontal(): + """Sobel on an edge should be a horizontal line""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.sobel(image) + # Fudge the eroded points + i[np.abs(j) == 5] = 10000 + assert (np.all(result[i == 0] == 1)) + assert (np.all(result[np.abs(i) > 1] == 0)) - def test_01_02_vertical(self): - """Sobel on a vertical edge should be a vertical line""" - i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.sobel(image) - j[np.abs(i) == 5] = 10000 - assert (np.all(result[j == 0] == 1)) - assert (np.all(result[np.abs(j) > 1] == 0)) +def test_sobel_vertical(): + """Sobel on a vertical edge should be a vertical line""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.sobel(image) + j[np.abs(i) == 5] = 10000 + assert (np.all(result[j == 0] == 1)) + assert (np.all(result[np.abs(j) > 1] == 0)) -class TestHSobel(): - def test_00_00_zeros(self): - """Horizontal sobel on an array of all zeros""" - result = F.hsobel(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) +def test_hsobel_zeros(): + """Horizontal sobel on an array of all zeros""" + result = F.hsobel(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) - def test_00_01_mask(self): - """Horizontal Sobel on a masked array should be zero""" - np.random.seed(0) - result = F.hsobel(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) +def test_hsobel_mask(): + """Horizontal Sobel on a masked array should be zero""" + np.random.seed(0) + result = F.hsobel(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) - def test_01_01_horizontal(self): - """Horizontal Sobel on an edge should be a horizontal line""" - i, j = np.mgrid[-5:6, -5:6] - image = (i >= 0).astype(float) - result = F.hsobel(image) - # Fudge the eroded points - i[np.abs(j) == 5] = 10000 - assert (np.all(result[i == 0] == 1)) - assert (np.all(result[np.abs(i) > 1] == 0)) +def test_hsobel_horizontal(): + """Horizontal Sobel on an edge should be a horizontal line""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.hsobel(image) + # Fudge the eroded points + i[np.abs(j) == 5] = 10000 + assert (np.all(result[i == 0] == 1)) + assert (np.all(result[np.abs(i) > 1] == 0)) - def test_01_02_vertical(self): - """Horizontal Sobel on a vertical edge should be zero""" - i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.hsobel(image) - assert (np.all(result == 0)) +def test_hsobel_vertical(): + """Horizontal Sobel on a vertical edge should be zero""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.hsobel(image) + assert (np.all(result == 0)) -class TestVSobel(): - def test_00_00_zeros(self): - """Vertical sobel on an array of all zeros""" - result = F.vsobel(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) +def test_vsobel_zeros(): + """Vertical sobel on an array of all zeros""" + result = F.vsobel(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) - def test_00_01_mask(self): - """Vertical Sobel on a masked array should be zero""" - np.random.seed(0) - result = F.vsobel(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) +def test_vsobel_mask(): + """Vertical Sobel on a masked array should be zero""" + np.random.seed(0) + result = F.vsobel(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) - def test_01_01_vertical(self): - """Vertical Sobel on an edge should be a vertical line""" - i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.vsobel(image) - # Fudge the eroded points - j[np.abs(i) == 5] = 10000 - assert (np.all(result[j == 0] == 1)) - assert (np.all(result[np.abs(j) > 1] == 0)) +def test_vsobel_vertical(): + """Vertical Sobel on an edge should be a vertical line""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.vsobel(image) + # Fudge the eroded points + j[np.abs(i) == 5] = 10000 + assert (np.all(result[j == 0] == 1)) + assert (np.all(result[np.abs(j) > 1] == 0)) - def test_01_02_horizontal(self): - """vertical Sobel on a horizontal edge should be zero""" - i, j = np.mgrid[-5:6, -5:6] - image = (i >= 0).astype(float) - result = F.vsobel(image) - eps = .000001 - assert (np.all(np.abs(result) < eps)) +def test_vsobel_horizontal(): + """vertical Sobel on a horizontal edge should be zero""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.vsobel(image) + eps = .000001 + assert (np.all(np.abs(result) < eps)) -class TestPrewitt(): - def test_00_00_zeros(self): - """Prewitt on an array of all zeros""" - result = F.prewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) +def test_prewitt_zeros(): + """Prewitt on an array of all zeros""" + result = F.prewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) - def test_00_01_mask(self): - """Prewitt on a masked array should be zero""" - np.random.seed(0) - result = F.prewitt(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - eps = .000001 - assert (np.all(np.abs(result) < eps)) +def test_prewitt_mask(): + """Prewitt on a masked array should be zero""" + np.random.seed(0) + result = F.prewitt(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + eps = .000001 + assert (np.all(np.abs(result) < eps)) - def test_01_01_horizontal(self): - """Prewitt on an edge should be a horizontal line""" - i, j = np.mgrid[-5:6, -5:6] - image = (i >= 0).astype(float) - result = F.prewitt(image) - # Fudge the eroded points - i[np.abs(j) == 5] = 10000 - eps = .000001 - assert (np.all(result[i == 0] == 1)) - assert (np.all(np.abs(result[np.abs(i) > 1]) < eps)) +def test_prewitt_horizontal(): + """Prewitt on an edge should be a horizontal line""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.prewitt(image) + # Fudge the eroded points + i[np.abs(j) == 5] = 10000 + eps = .000001 + assert (np.all(result[i == 0] == 1)) + assert (np.all(np.abs(result[np.abs(i) > 1]) < eps)) - def test_01_02_vertical(self): - """Prewitt on a vertical edge should be a vertical line""" - i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.prewitt(image) - eps = .000001 - j[np.abs(i) == 5] = 10000 - assert (np.all(result[j == 0] == 1)) - assert (np.all(np.abs(result[np.abs(j) > 1]) < eps)) +def test_prewitt_vertical(): + """Prewitt on a vertical edge should be a vertical line""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.prewitt(image) + eps = .000001 + j[np.abs(i) == 5] = 10000 + assert (np.all(result[j == 0] == 1)) + assert (np.all(np.abs(result[np.abs(j) > 1]) < eps)) -class TestHPrewitt(): - def test_00_00_zeros(self): - """Horizontal sobel on an array of all zeros""" - result = F.hprewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) +def test_hprewitt_zeros(): + """Horizontal prewitt on an array of all zeros""" + result = F.hprewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) - def test_00_01_mask(self): - """Horizontal prewitt on a masked array should be zero""" - np.random.seed(0) - result = F.hprewitt(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - eps = .000001 - assert (np.all(np.abs(result) < eps)) +def test_hprewitt_mask(): + """Horizontal prewitt on a masked array should be zero""" + np.random.seed(0) + result = F.hprewitt(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + eps = .000001 + assert (np.all(np.abs(result) < eps)) - def test_01_01_horizontal(self): - """Horizontal prewitt on an edge should be a horizontal line""" - i, j = np.mgrid[-5:6, -5:6] - image = (i >= 0).astype(float) - result = F.hprewitt(image) - # Fudge the eroded points - i[np.abs(j) == 5] = 10000 - eps = .000001 - assert (np.all(result[i == 0] == 1)) - assert (np.all(np.abs(result[np.abs(i) > 1]) < eps)) +def test_hprewitt_horizontal(): + """Horizontal prewitt on an edge should be a horizontal line""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.hprewitt(image) + # Fudge the eroded points + i[np.abs(j) == 5] = 10000 + eps = .000001 + assert (np.all(result[i == 0] == 1)) + assert (np.all(np.abs(result[np.abs(i) > 1]) < eps)) - def test_01_02_vertical(self): - """Horizontal prewitt on a vertical edge should be zero""" - i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.hprewitt(image) - eps = .000001 - assert (np.all(np.abs(result) < eps)) +def test_hprewitt_vertical(): + """Horizontal prewitt on a vertical edge should be zero""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.hprewitt(image) + eps = .000001 + assert (np.all(np.abs(result) < eps)) -class TestVPrewitt(): - def test_00_00_zeros(self): - """Vertical prewitt on an array of all zeros""" - result = F.vprewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) - assert (np.all(result == 0)) +def test_vprewitt_zeros(): + """Vertical prewitt on an array of all zeros""" + result = F.vprewitt(np.zeros((10, 10)), np.ones((10, 10), bool)) + assert (np.all(result == 0)) - def test_00_01_mask(self): - """Vertical prewitt on a masked array should be zero""" - np.random.seed(0) - result = F.vprewitt(np.random.uniform(size=(10, 10)), - np.zeros((10, 10), bool)) - assert (np.all(result == 0)) +def test_vprewitt_mask(): + """Vertical prewitt on a masked array should be zero""" + np.random.seed(0) + result = F.vprewitt(np.random.uniform(size=(10, 10)), + np.zeros((10, 10), bool)) + assert (np.all(result == 0)) - def test_01_01_vertical(self): - """Vertical prewitt on an edge should be a vertical line""" - i, j = np.mgrid[-5:6, -5:6] - image = (j >= 0).astype(float) - result = F.vprewitt(image) - # Fudge the eroded points - j[np.abs(i) == 5] = 10000 - assert (np.all(result[j == 0] == 1)) - eps = .000001 - assert (np.all(np.abs(result[np.abs(j) > 1]) < eps)) +def test_vprewitt_vertical(): + """Vertical prewitt on an edge should be a vertical line""" + i, j = np.mgrid[-5:6, -5:6] + image = (j >= 0).astype(float) + result = F.vprewitt(image) + # Fudge the eroded points + j[np.abs(i) == 5] = 10000 + assert (np.all(result[j == 0] == 1)) + eps = .000001 + assert (np.all(np.abs(result[np.abs(j) > 1]) < eps)) - def test_01_02_horizontal(self): - """Vertical prewitt on a horizontal edge should be zero""" - i, j = np.mgrid[-5:6, -5:6] - image = (i >= 0).astype(float) - result = F.vprewitt(image) - eps = .000001 - assert (np.all(np.abs(result) < eps)) +def test_vprewitt_horizontal(): + """Vertical prewitt on a horizontal edge should be zero""" + i, j = np.mgrid[-5:6, -5:6] + image = (i >= 0).astype(float) + result = F.vprewitt(image) + eps = .000001 + assert (np.all(np.abs(result) < eps)) if __name__ == "__main__": From 87a3025196b0b3429cab1f439cd10728e99d982f Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 3 Sep 2012 10:09:13 -0700 Subject: [PATCH 477/648] BUG: Add missing imports in transform module. --- skimage/transform/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 48a252b2..a25ce969 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -6,4 +6,5 @@ from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) -from ._warps import swirl, homography +from ._warps import swirl, homography, resize, rotate + From 7983f354b80bc7cdc8c0ae39cbebd8ca3ac07060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 3 Sep 2012 21:17:46 +0200 Subject: [PATCH 478/648] Apply PEP8 and improve docs --- skimage/transform/_geometric.py | 164 ++++++++++++++++---------------- 1 file changed, 84 insertions(+), 80 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 030197da..54764742 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -303,6 +303,85 @@ class AffineTransform(ProjectiveTransform): return self._matrix[0:2, 2] +class PiecewiseAffineTransform(ProjectiveTransform): + + """2D piecewise affine transformation. + + Control points are used to define the mapping. The transform is based on + a Delaunay triangulation of the points to form a mesh. Each triangle is + used to find a local affine transform. + + Parameters + ---------- + TODO + + """ + + def __init__(self): + self.tesselation = None + self.affines = [] + self._matrix = None + + def estimate(self, src, dst): + """Set the control points with which to perform the piecewise mapping. + + Number of source and destination coordinates must match. + + Parameters + ---------- + src : (N, 2) array + Source coordinates. + dst : (N, 2) array + Destination coordinates. + + """ + + # triangulate input positions into mesh + self.tesselation = spatial.Delaunay(src) + + # find affine mapping from source positions to destination + self.affines = [] + for tri in self.tesselation.vertices: + affine = AffineTransform() + affine.estimate(src[tri, :], dst[tri, :]) + self.affines.append(affine) + + def __call__(self, coords): + """Apply forward transformation. + + Parameters + ---------- + coords : (N, 2) array + source coordinates + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + + out = - 1 * np.ones((coords.shape[0], 2)) + + for index, pt in enumerate(coords): + # determine which triangle contains the point + simplex_index = self.tesselation.find_simplex(pt) + + if simplex_index == - 1: + # this point is outside the hull of the control points + out[index, 0] = - 1 + out[index, 1] = - 1 + continue + + # calculate affine transformed position + affine = self.affines[simplex_index] + dst_pos = affine(pt) + out[index, 0] = dst_pos[0][0] + out[index, 1] = dst_pos[0][1] + + return out + + class SimilarityTransform(ProjectiveTransform): """2D similarity transformation of the form:: @@ -580,90 +659,13 @@ class PolynomialTransform(GeometricTransform): 'parameters by exchanging source and destination coordinates,' 'then apply the forward transformation.') -class PiecewiseAffineTransform(ProjectiveTransform): - - """2D piecewise affine transformation. - - Control points are used to define the mapping. The transform is based on - a Delaunay triangulation of the points to form a mesh. Each triangle is - used to find a local affine transform. - - Parameters - ---------- - TODO - - """ - - def __init__(self): - self.tess = None - self.triAffines = [] - self._matrix = None - - def estimate(self, src, dst): - """Set the control points with which to perform the piecewise affine mapping. - - Number of source and destination coordinates must match. - - Parameters - ---------- - src : (N, 2) array - Source coordinates. - dst : (N, 2) array - Destination coordinates. - - """ - - #Triangulate input positions into mesh - self.tess = spatial.Delaunay(src) - - #Find affine mapping from source positions to destination - self.triAffines = [] - for tri in self.tess.vertices: - affine = AffineTransform() - affine.estimate(src[tri,:], dst[tri,:]) - self.triAffines.append(affine) - - def __call__(self, coords): - """Apply forward transformation. - - Parameters - ---------- - coords : (N, 2) array - source coordinates - - Returns - ------- - coords : (N, 2) array - Transformed coordinates. - - """ - - out = np.ones((coords.shape[0], 2)) * -1 - - for ptNum, pt in enumerate(coords): - #Determine which triangle contains the point - simplexIndex = self.tess.find_simplex(pt) - - if simplexIndex == -1: - #This point is outside the hull of the control points - out[ptNum,0] = -1 - out[ptNum,1] = -1 - continue - - #Calculate affine transformed position - affine = self.triAffines[simplexIndex] - destPos = affine(pt) - out[ptNum,0] = destPos[0][0] - out[ptNum,1] = destPos[0][1] - - return out TRANSFORMS = { 'similarity': SimilarityTransform, 'affine': AffineTransform, + 'piecewise-affine': PiecewiseAffineTransform, 'projective': ProjectiveTransform, 'polynomial': PolynomialTransform, - 'piecewiseaffine': PiecewiseAffineTransform, } HOMOGRAPHY_TRANSFORMS = ( SimilarityTransform, @@ -671,6 +673,7 @@ HOMOGRAPHY_TRANSFORMS = ( ProjectiveTransform ) + def estimate_transform(ttype, src, dst, **kwargs): """Estimate 2D geometric transformation parameters. @@ -681,7 +684,8 @@ def estimate_transform(ttype, src, dst, **kwargs): Parameters ---------- - ttype : {'similarity', 'affine', 'piecewiseaffine', 'projective', 'polynomial'} + ttype : {'similarity', 'affine', 'piecewise-affine', 'projective', \ + 'polynomial'} Type of transform. kwargs : array or int Function parameters (src, dst, n, angle):: @@ -689,7 +693,7 @@ def estimate_transform(ttype, src, dst, **kwargs): NAME / TTYPE FUNCTION PARAMETERS 'similarity' `src, `dst` 'affine' `src, `dst` - 'piecewiseaffine' `src, `dst` + 'piecewise-affine' `src, `dst` 'projective' `src, `dst` 'polynomial' `src, `dst`, `order` (polynomial order) From c1f3515e0065e5aa2af51def5d5300a4aafef8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 3 Sep 2012 21:29:19 +0200 Subject: [PATCH 479/648] Speed up transformation of piecewise-affine --- skimage/transform/_geometric.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 54764742..6d64ec5c 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -361,23 +361,19 @@ class PiecewiseAffineTransform(ProjectiveTransform): """ - out = - 1 * np.ones((coords.shape[0], 2)) + out = np.empty_like(coords) - for index, pt in enumerate(coords): - # determine which triangle contains the point - simplex_index = self.tesselation.find_simplex(pt) + simplex = self.tesselation.find_simplex(coords) - if simplex_index == - 1: - # this point is outside the hull of the control points - out[index, 0] = - 1 - out[index, 1] = - 1 - continue + out[simplex == -1, :] = -1 - # calculate affine transformed position - affine = self.affines[simplex_index] - dst_pos = affine(pt) - out[index, 0] = dst_pos[0][0] - out[index, 1] = dst_pos[0][1] + for index in range(len(self.tesselation.vertices)): + # affine transform for triangle + affine = self.affines[index] + # all coordinates within triangle + index_mask = simplex == index + + out[index_mask, :] = affine(coords[index_mask, :]) return out From 33151f4349bdc899b817942b255a7b15c49605f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 3 Sep 2012 21:40:45 +0200 Subject: [PATCH 480/648] Add missing doc string for inverse transform --- skimage/transform/_geometric.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 6d64ec5c..ab530852 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -111,6 +111,19 @@ class ProjectiveTransform(GeometricTransform): return self._apply_mat(coords, self._matrix) def inverse(self, coords): + """Apply inverse transformation. + + Parameters + ---------- + coords : (N, 2) array + Source coordinates. + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ return self._apply_mat(coords, self._inv_matrix) def estimate(self, src, dst): From d541ebac17d764f4ccaac440322c8d371a20edf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 3 Sep 2012 21:41:16 +0200 Subject: [PATCH 481/648] Implement inverse transformation for piecewise-affine --- skimage/transform/_geometric.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index ab530852..e4913d4b 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -390,6 +390,37 @@ class PiecewiseAffineTransform(ProjectiveTransform): return out + def inverse(self, coords): + """Apply inverse transformation. + + Parameters + ---------- + coords : (N, 2) array + Source coordinates. + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + + out = np.empty_like(coords) + + simplex = self.tesselation.find_simplex(coords) + + out[simplex == -1, :] = -1 + + for index in range(len(self.tesselation.vertices)): + # affine transform for triangle + affine = self.affines[index] + # all coordinates within triangle + index_mask = simplex == index + + out[index_mask, :] = affine.inverse(coords[index_mask, :]) + + return out + class SimilarityTransform(ProjectiveTransform): """2D similarity transformation of the form:: From 3f7d962206b175265054c284dbe59d3af5a7e783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 3 Sep 2012 21:41:53 +0200 Subject: [PATCH 482/648] Remove unnecessary params section in doc string --- skimage/transform/_geometric.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index e4913d4b..6c0afbdb 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -324,16 +324,11 @@ class PiecewiseAffineTransform(ProjectiveTransform): a Delaunay triangulation of the points to form a mesh. Each triangle is used to find a local affine transform. - Parameters - ---------- - TODO - """ def __init__(self): self.tesselation = None self.affines = [] - self._matrix = None def estimate(self, src, dst): """Set the control points with which to perform the piecewise mapping. From 65879a2cdef723a2ec870dd7d62d8e1fde01c17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 3 Sep 2012 21:46:14 +0200 Subject: [PATCH 483/648] Fix fast warping of images --- skimage/transform/_geometric.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 6c0afbdb..cfa12033 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -954,9 +954,10 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # use fast Cython version for specific interpolation orders if order in range(4) and not map_args: matrix = None - if isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): + if inverse_map in HOMOGRAPHY_TRANSFORMS: matrix = inverse_map._matrix - elif inverse_map.__name__ == 'inverse' \ + elif hasattr(inverse_map, '__name__') \ + and inverse_map.__name__ == 'inverse' \ and inverse_map.im_class in HOMOGRAPHY_TRANSFORMS: matrix = np.linalg.inv(inverse_map.im_self._matrix) if matrix is not None: From f79012fe2264642085aee80a176b27bf12602808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 3 Sep 2012 21:49:14 +0200 Subject: [PATCH 484/648] Add note about coordinates outside of mesh --- skimage/transform/_geometric.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index cfa12033..e2975def 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -357,10 +357,12 @@ class PiecewiseAffineTransform(ProjectiveTransform): def __call__(self, coords): """Apply forward transformation. + Coordinates outside of the mesh will be set to `- 1`. + Parameters ---------- coords : (N, 2) array - source coordinates + Source coordinates. Returns ------- @@ -388,6 +390,8 @@ class PiecewiseAffineTransform(ProjectiveTransform): def inverse(self, coords): """Apply inverse transformation. + Coordinates outside of the mesh will be set to `- 1`. + Parameters ---------- coords : (N, 2) array From 677478873c2d2e6a439b0bf443e3524f6d3cba41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 3 Sep 2012 22:04:57 +0200 Subject: [PATCH 485/648] Fix inverse piecewise affine --- skimage/transform/_geometric.py | 41 +++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index e2975def..f81e6e22 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -327,8 +327,10 @@ class PiecewiseAffineTransform(ProjectiveTransform): """ def __init__(self): - self.tesselation = None + self._tesselation = None + self._inverse_tesselation = None self.affines = [] + self.inverse_affines = [] def estimate(self, src, dst): """Set the control points with which to perform the piecewise mapping. @@ -344,16 +346,27 @@ class PiecewiseAffineTransform(ProjectiveTransform): """ + # forward piecewise affine # triangulate input positions into mesh - self.tesselation = spatial.Delaunay(src) - + self._tesselation = spatial.Delaunay(src) # find affine mapping from source positions to destination self.affines = [] - for tri in self.tesselation.vertices: + for tri in self._tesselation.vertices: affine = AffineTransform() affine.estimate(src[tri, :], dst[tri, :]) self.affines.append(affine) + # inverse piecewise affine + # triangulate input positions into mesh + self._inverse_tesselation = spatial.Delaunay(dst) + # find affine mapping from source positions to destination + self.inverse_affines = [] + for tri in self._inverse_tesselation.vertices: + affine = AffineTransform() + affine.estimate(dst[tri, :], src[tri, :]) + self.inverse_affines.append(affine) + + def __call__(self, coords): """Apply forward transformation. @@ -371,13 +384,15 @@ class PiecewiseAffineTransform(ProjectiveTransform): """ - out = np.empty_like(coords) + out = np.empty_like(coords, np.double) - simplex = self.tesselation.find_simplex(coords) + # determine triangle index for each coordinate + simplex = self._tesselation.find_simplex(coords) + # coordinates outside of mesh out[simplex == -1, :] = -1 - for index in range(len(self.tesselation.vertices)): + for index in range(len(self._tesselation.vertices)): # affine transform for triangle affine = self.affines[index] # all coordinates within triangle @@ -404,19 +419,21 @@ class PiecewiseAffineTransform(ProjectiveTransform): """ - out = np.empty_like(coords) + out = np.empty_like(coords, np.double) - simplex = self.tesselation.find_simplex(coords) + # determine triangle index for each coordinate + simplex = self._inverse_tesselation.find_simplex(coords) + # coordinates outside of mesh out[simplex == -1, :] = -1 - for index in range(len(self.tesselation.vertices)): + for index in range(len(self._inverse_tesselation.vertices)): # affine transform for triangle - affine = self.affines[index] + affine = self.inverse_affines[index] # all coordinates within triangle index_mask = simplex == index - out[index_mask, :] = affine.inverse(coords[index_mask, :]) + out[index_mask, :] = affine(coords[index_mask, :]) return out From 05ae17e0d5cc999e88a3e73993c495b72e2e3383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 3 Sep 2012 22:05:12 +0200 Subject: [PATCH 486/648] Add test cases for piecewise affine --- skimage/transform/tests/test_geometric.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index 57e95d46..d3a75f7b 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -3,7 +3,8 @@ from numpy.testing import assert_equal, assert_array_almost_equal from skimage.transform._geometric import _stackcopy from skimage.transform import (estimate_transform, SimilarityTransform, AffineTransform, - ProjectiveTransform, PolynomialTransform) + ProjectiveTransform, PolynomialTransform, + PiecewiseAffineTransform) SRC = np.array([ @@ -110,6 +111,14 @@ def test_affine_init(): assert_array_almost_equal(tform2.translation, translation) +def test_piecewise_affine(): + tform = PiecewiseAffineTransform() + tform.estimate(SRC, DST) + # make sure each single affine transform is exactly estimated + assert_array_almost_equal(tform(SRC), DST) + assert_array_almost_equal(tform.inverse(DST), SRC) + + def test_projective_estimation(): # exact solution tform = estimate_transform('projective', SRC[:4, :], DST[:4, :]) From e16a6d32dc4919db711bc5963601179aa4e3019c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 3 Sep 2012 22:07:10 +0200 Subject: [PATCH 487/648] Fix typo in polygon example script doc --- doc/examples/plot_polygon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/examples/plot_polygon.py b/doc/examples/plot_polygon.py index 5b745fed..05ca5359 100644 --- a/doc/examples/plot_polygon.py +++ b/doc/examples/plot_polygon.py @@ -1,7 +1,7 @@ """ -=================================== -Approximatea and subdivide polygons -=================================== +================================== +Approximate and subdivide polygons +================================== This example shows how to approximate (Douglas-Peucker algorithm) and subdivide (B-Splines) polygonal chains. From 53edb67dd3a7a88d9133a85cd2fe1a9d22a3a436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 3 Sep 2012 22:49:09 +0200 Subject: [PATCH 488/648] Add example script for piecewise affine transform --- doc/examples/plot_piecewise_affine.py | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 doc/examples/plot_piecewise_affine.py diff --git a/doc/examples/plot_piecewise_affine.py b/doc/examples/plot_piecewise_affine.py new file mode 100644 index 00000000..9db0e402 --- /dev/null +++ b/doc/examples/plot_piecewise_affine.py @@ -0,0 +1,38 @@ +""" +=============================== +Piecewise Affine Transformation +=============================== + +This example shows how to use the Piecewise Affine Transformation. +""" + +import numpy as np +import matplotlib.pyplot as plt +from skimage.transform import PiecewiseAffineTransform, warp +from skimage import data + + +image = data.lena() +rows, cols = image.shape[0], image.shape[1] + +src_cols = np.linspace(0, cols, 20) +src_rows = np.linspace(0, rows, 20) +src_rows, src_cols = np.meshgrid(src_rows, src_cols) +src = np.dstack([src_cols.flat, src_rows.flat])[0] + +# add sinusoidal oscillation to row coordinates +dst_rows = src[:, 1] - np.sin(np.linspace(0, 3 * np.pi, src.shape[0])) * 50 +dst_cols = src[:, 0] +dst_rows *= 1.5 +dst_rows -= 1.5 * 50 +dst = np.vstack([dst_cols, dst_rows]).T + + +tform = PiecewiseAffineTransform() +tform.estimate(src, dst) + +output_shape = (image.shape[0] - 1.5 * 50, image.shape[1]) +out = warp(image, tform, output_shape=output_shape) + +plt.imshow(out) +plt.show() From aff606998eccb328a48323f79d26d6c96ad4900a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 3 Sep 2012 23:06:59 +0200 Subject: [PATCH 489/648] Add mesh points to plot --- doc/examples/plot_piecewise_affine.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/examples/plot_piecewise_affine.py b/doc/examples/plot_piecewise_affine.py index 9db0e402..2dcbd9f1 100644 --- a/doc/examples/plot_piecewise_affine.py +++ b/doc/examples/plot_piecewise_affine.py @@ -16,7 +16,7 @@ image = data.lena() rows, cols = image.shape[0], image.shape[1] src_cols = np.linspace(0, cols, 20) -src_rows = np.linspace(0, rows, 20) +src_rows = np.linspace(0, rows, 10) src_rows, src_cols = np.meshgrid(src_rows, src_cols) src = np.dstack([src_cols.flat, src_rows.flat])[0] @@ -31,8 +31,11 @@ dst = np.vstack([dst_cols, dst_rows]).T tform = PiecewiseAffineTransform() tform.estimate(src, dst) -output_shape = (image.shape[0] - 1.5 * 50, image.shape[1]) -out = warp(image, tform, output_shape=output_shape) +out_rows = image.shape[0] - 1.5 * 50 +out_cols = cols +out = warp(image, tform, output_shape=(out_rows, out_cols)) plt.imshow(out) +plt.plot(tform.inverse(src)[:, 0], tform.inverse(src)[:, 1], '.b') +plt.axis((0, out_cols, out_rows, 0)) plt.show() From 852481e0550aeded06c1e81562f93037c5636cd5 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 3 Sep 2012 22:27:51 -0400 Subject: [PATCH 490/648] TST: Add tests for masked region --- skimage/filter/tests/test_edges.py | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/skimage/filter/tests/test_edges.py b/skimage/filter/tests/test_edges.py index 7fe8da4b..a5d52aa5 100644 --- a/skimage/filter/tests/test_edges.py +++ b/skimage/filter/tests/test_edges.py @@ -1,4 +1,5 @@ import numpy as np +from numpy.testing import assert_array_almost_equal as assert_close import skimage.filter as F @@ -196,6 +197,40 @@ def test_vprewitt_horizontal(): assert (np.all(np.abs(result) < eps)) +def test_horizontal_mask_line(): + """Horizontal edge filters mask pixels surrounding input mask.""" + vgrad, _ = np.mgrid[:1:11j, :1:11j] # vertical gradient with spacing 0.1 + vgrad[5, :] = 1 # bad horizontal line + + mask = np.ones_like(vgrad) + mask[5, :] = 0 # mask bad line + + expected = np.zeros_like(vgrad) + expected[1:-1, 1:-1] = 0.2 # constant gradient for most of image, + expected[4:7, 1:-1] = 0 # but line and neighbors masked + + for grad_func in (F.hprewitt, F.hsobel): + result = grad_func(vgrad, mask) + yield assert_close, result, expected + + +def test_vertical_mask_line(): + """Vertical edge filters mask pixels surrounding input mask.""" + _, hgrad = np.mgrid[:1:11j, :1:11j] # horizontal gradient with spacing 0.1 + hgrad[:, 5] = 1 # bad vertical line + + mask = np.ones_like(hgrad) + mask[:, 5] = 0 # mask bad line + + expected = np.zeros_like(hgrad) + expected[1:-1, 1:-1] = 0.2 # constant gradient for most of image, + expected[1:-1, 4:7] = 0 # but line and neighbors masked + + for grad_func in (F.vprewitt, F.vsobel): + result = grad_func(hgrad, mask) + yield assert_close, result, expected + + if __name__ == "__main__": from numpy import testing testing.run_module_suite() From bd2f8ac3d3c7c5413bb9f01724a536d0ab92af8b Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 3 Sep 2012 22:36:11 -0400 Subject: [PATCH 491/648] DOC: Add note about expanded masking --- skimage/filter/edges.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index cd91effb..ac27802f 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -22,6 +22,8 @@ def sobel(image, mask=None): Image to process. mask : array_like, dtype=bool, optional An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. Returns ------- @@ -49,6 +51,8 @@ def hsobel(image, mask=None): Image to process. mask : array_like, dtype=bool, optional An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. Returns ------- @@ -88,6 +92,8 @@ def vsobel(image, mask=None): Image to process mask : array_like, dtype=bool, optional An optional mask to limit the application to a certain area + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. Returns ------- @@ -127,6 +133,8 @@ def prewitt(image, mask=None): Image to process. mask : array_like, dtype=bool, optional An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. Returns ------- @@ -150,6 +158,8 @@ def hprewitt(image, mask=None): Image to process. mask : array_like, dtype=bool, optional An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. Returns ------- @@ -189,6 +199,8 @@ def vprewitt(image, mask=None): Image to process. mask : array_like, dtype=bool, optional An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. Returns ------- From 6c9810c6e9fd5105b70271108f494448bd6e7da5 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 3 Sep 2012 22:50:57 -0400 Subject: [PATCH 492/648] Refactor masking --- skimage/filter/edges.py | 50 ++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index ac27802f..1568886c 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -13,6 +13,24 @@ from skimage import img_as_float from scipy.ndimage import convolve, binary_erosion, generate_binary_structure +EROSION_SELEM = generate_binary_structure(2, 2) + + +def _mask_filter_result(result, mask): + """Return result after masking. + + Input masks are eroded so that mask areas in the original image don't + affect values in the result. + """ + if mask is None: + mask = np.zeros(result.shape, bool) + mask[1:-1, 1:-1] = True + else: + mask = binary_erosion(mask, EROSION_SELEM, border_value=0) + result[mask == False] = 0 + return result + + def sobel(image, mask=None): """Calculate the absolute magnitude Sobel to find edges. @@ -70,17 +88,11 @@ def hsobel(image, mask=None): """ image = img_as_float(image) - if mask is None: - mask = np.ones(image.shape, bool) - big_mask = binary_erosion(mask, - generate_binary_structure(2, 2), - border_value=0) result = np.abs(convolve(image, np.array([[ 1, 2, 1], [ 0, 0, 0], [-1,-2,-1]]).astype(float) / 4.0)) - result[big_mask == False] = 0 - return result + return _mask_filter_result(result, mask) def vsobel(image, mask=None): @@ -111,17 +123,11 @@ def vsobel(image, mask=None): """ image = img_as_float(image) - if mask is None: - mask = np.ones(image.shape, bool) - big_mask = binary_erosion(mask, - generate_binary_structure(2, 2), - border_value=0) result = np.abs(convolve(image, np.array([[1, 0, -1], [2, 0, -2], [1, 0, -1]]).astype(float) / 4.0)) - result[big_mask == False] = 0 - return result + return _mask_filter_result(result, mask) def prewitt(image, mask=None): @@ -177,17 +183,11 @@ def hprewitt(image, mask=None): """ image = img_as_float(image) - if mask is None: - mask = np.ones(image.shape, bool) - big_mask = binary_erosion(mask, - generate_binary_structure(2, 2), - border_value=0) result = np.abs(convolve(image, np.array([[ 1, 1, 1], [ 0, 0, 0], [-1,-1,-1]]).astype(float) / 3.0)) - result[big_mask == False] = 0 - return result + return _mask_filter_result(result, mask) def vprewitt(image, mask=None): @@ -218,14 +218,8 @@ def vprewitt(image, mask=None): """ image = img_as_float(image) - if mask is None: - mask = np.ones(image.shape, bool) - big_mask = binary_erosion(mask, - generate_binary_structure(2, 2), - border_value=0) result = np.abs(convolve(image, np.array([[1, 0, -1], [1, 0, -1], [1, 0, -1]]).astype(float) / 3.0)) - result[big_mask == False] = 0 - return result + return _mask_filter_result(result, mask) From 367e39094c3658e0e63f84f8c334934c4967a77b Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 4 Sep 2012 09:35:35 -0400 Subject: [PATCH 493/648] STY: Rework masking based on suggestions by @ahojnnes --- skimage/filter/edges.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index 1568886c..134aa796 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -23,12 +23,14 @@ def _mask_filter_result(result, mask): affect values in the result. """ if mask is None: - mask = np.zeros(result.shape, bool) - mask[1:-1, 1:-1] = True + result[0, :] = 0 + result[-1, :] = 0 + result[:, 0] = 0 + result[:, -1] = 0 + return result else: mask = binary_erosion(mask, EROSION_SELEM, border_value=0) - result[mask == False] = 0 - return result + return result * mask def sobel(image, mask=None): From d7bf0c5fc11d75f7785d2fa73c81d36dbb5e2ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 4 Sep 2012 17:40:54 +0200 Subject: [PATCH 494/648] Add Coordinates property to property list --- skimage/measure/_regionprops.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 5615ecc5..142550bf 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -22,6 +22,7 @@ PROPS = ( 'ConvexArea', # 'ConvexHull', 'ConvexImage', + 'Coordinates', 'Eccentricity', 'EquivDiameter', 'EulerNumber', From fe4102337065a26b552cdaffea037e228ebf9824 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 4 Sep 2012 22:07:25 -0400 Subject: [PATCH 495/648] STY: Refactor initialization of QApplication. --- skimage/viewer/utils/core.py | 21 +++++++++++++++++++-- skimage/viewer/viewers/core.py | 16 +++++----------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 5c5f878c..74fd333f 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -17,8 +17,25 @@ except ImportError: print("Could not import PyQt4 -- skimage.viewer not available.") -__all__ = ['figimage', 'LinearColormap', 'ClearColormap', 'MatplotlibCanvas', - 'RequiredAttr'] +__all__ = ['init_qtapp', 'start_qtapp', 'RequiredAttr', 'figimage', + 'LinearColormap', 'ClearColormap', 'MatplotlibCanvas'] + + +QApp = None + + +def init_qtapp(): + """Initialize QAppliction. + + The QApplication needs to be initialized before creating any QWidgets + """ + global QApp + if QApp is None: + QApp = QtGui.QApplication([]) + +def start_qtapp(): + """Start Qt mainloop""" + QApp.exec_() class RequiredAttr(object): diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index d29f950e..7c4b4753 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -1,8 +1,6 @@ """ ImageViewer class for viewing and interacting with images. """ -import sys - try: from PyQt4 import QtGui, QtCore from PyQt4.QtGui import QMainWindow @@ -11,20 +9,18 @@ except ImportError: print("Could not import PyQt4 -- skimage.viewer not available.") from skimage.util.dtype import dtype_range -from ..utils import figimage, MatplotlibCanvas +from .. import utils from ..widgets import Slider __all__ = ['ImageViewer', 'CollectionViewer'] -qApp = None - -class ImageCanvas(MatplotlibCanvas): +class ImageCanvas(utils.MatplotlibCanvas): """Canvas for displaying images.""" def __init__(self, parent, image, **kwargs): - self.fig, self.ax = figimage(image, **kwargs) + self.fig, self.ax = utils.figimage(image, **kwargs) super(ImageCanvas, self).__init__(parent, self.fig, **kwargs) @@ -60,9 +56,7 @@ class ImageViewer(QMainWindow): """ def __init__(self, image): # Start main loop - global qApp - if qApp is None: - qApp = QtGui.QApplication(sys.argv) + utils.init_qtapp() super(ImageViewer, self).__init__() #TODO: Add ImageViewer to skimage.io window manager @@ -136,7 +130,7 @@ class ImageViewer(QMainWindow): for p in self.plugins: p.show() super(ImageViewer, self).show() - qApp.exec_() + utils.start_qtapp() def redraw(self): self.canvas.draw_idle() From d1e012ea30798fe0fa636a18aca7deb9e0525420 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 4 Sep 2012 22:07:59 -0400 Subject: [PATCH 496/648] BUG: Initialize QApplication when creating Plugin. QWidgets cannot be initialized unless QApplication has been created. In cases where the Plugin is created before the ImageViewer, ensure that QApplication exists. --- skimage/viewer/plugins/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index ae240c20..074e18d4 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -14,7 +14,7 @@ try: except ImportError: print("Could not import matplotlib -- skimage.viewer not available.") -from ..utils import RequiredAttr +from ..utils import RequiredAttr, init_qtapp class Plugin(QDialog): @@ -82,6 +82,7 @@ class Plugin(QDialog): draws_on_image = False def __init__(self, image_filter=None, height=0, width=400, useblit=None): + init_qtapp() super(Plugin, self).__init__() self.image_viewer = None From 0344e51d405a5ddc6816f77deb543570449ead11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 5 Sep 2012 15:03:14 +0200 Subject: [PATCH 497/648] Add function to build gaussian and laplacian pyramids --- skimage/transform/__init__.py | 5 +- skimage/transform/pyramid.py | 292 ++++++++++++++++++++++++++++++++++ 2 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 skimage/transform/pyramid.py diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index a25ce969..abf31517 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -4,7 +4,8 @@ from .finite_radon_transform import * from .integral import * from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, - ProjectiveTransform, PolynomialTransform, + ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) from ._warps import swirl, homography, resize, rotate - +from .pyramid import (pyramid_reduce, pyramid_expand, + build_gaussian_pyramid, build_laplacian_pyramid) diff --git a/skimage/transform/pyramid.py b/skimage/transform/pyramid.py new file mode 100644 index 00000000..1a011fb6 --- /dev/null +++ b/skimage/transform/pyramid.py @@ -0,0 +1,292 @@ +import math +import numpy as np +from scipy import ndimage +from skimage.transform import resize +from skimage.util import img_as_float + + +def _smooth(image, sigma, mode, cval): + + # allocate output array + smoothed = np.empty(image.shape, dtype=np.double) + + if image.ndim == 3: # apply gaussian filter to all dimensions independently + for dim in range(image.shape[2]): + ndimage.gaussian_filter(image[..., dim], sigma, + output=smoothed[..., dim], + mode=mode, cval=cval) + else: + ndimage.gaussian_filter(image, sigma, output=smoothed, + mode=mode, cval=cval) + + return smoothed + + +def _check_factor(factor): + if factor <= 1: + raise ValueError('scale factor must be greater than 1') + + +def pyramid_reduce(image, factor=2, sigma=None, order=1, + mode='reflect', cval=0): + """Smooth and then downsample image. + + Parameters + ---------- + image : array + Input image. + factor : float, optional + Downscale factor. Default is 2. + sigma : float, optional + Sigma for gaussian filter. Default is `2 * factor / 6.0` which + corresponds to a filter mask twice the size of the scale factor that + covers more than 99% of the gaussian distribution. + order : int, optional + Order of splines used in interpolation of downsampling. See + `scipy.ndimage.map_coordinates` for detail. Default is 1. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + Default is 'reflect'. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. Default is 0. + + Returns + ------- + out : array + Smoothed and downsampled image. + + References + ---------- + ..[1] http://web.mit.edu/persci/people/adelson/pub_pdfs/pyramid83.pdf + + """ + + _check_factor(factor) + + image = img_as_float(image) + + rows = image.shape[0] + cols = image.shape[1] + out_rows = math.ceil(rows / float(factor)) + out_cols = math.ceil(cols / float(factor)) + + if sigma is None: + # automatically determine sigma which covers > 99% of distribution + sigma = 2 * factor / 6.0 + + smoothed = _smooth(image, sigma, mode, cval) + out = resize(smoothed, (out_rows, out_cols), order=order, + mode=mode, cval=cval) + + return out + + +def pyramid_expand(image, factor=2, sigma=None, order=1, + mode='reflect', cval=0): + """Upsample and then smooth image. + + Parameters + ---------- + image : array + Input image. + factor : float, optional + Upscale factor. Default is 2. + sigma : float, optional + Sigma for gaussian filter. Default is `2 * factor / 6.0` which + corresponds to a filter mask twice the size of the scale factor that + covers more than 99% of the gaussian distribution. + order : int, optional + Order of splines used in interpolation of downsampling. See + `scipy.ndimage.map_coordinates` for detail. Default is 1. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + Default is 'reflect'. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. Default is 0. + + Returns + ------- + out : array + Upsampled and smoothed image. + + References + ---------- + ..[1] http://web.mit.edu/persci/people/adelson/pub_pdfs/pyramid83.pdf + + """ + + _check_factor(factor) + + rows = image.shape[0] + cols = image.shape[1] + out_rows = 2 * rows + out_cols = 2 * cols + + if sigma is None: + # automatically determine sigma which covers > 99% of distribution + sigma = 2 * factor / 6.0 + + resized = resize(image, (out_rows, out_cols), order=order, + mode=mode, cval=cval) + out = _smooth(resized, sigma, mode, cval) + + return out + + +def build_gaussian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, + mode='reflect', cval=0): + """Build gaussian pyramid. + + Recursively applies the `pyramid_reduce` function to the image. + + Parameters + ---------- + image : array + Input image. + max_layer : int + Number of layers for the pyramid. 0th layer is the original image. + Default is -1 which builds all possible layers. + factor : float, optional + Downscale factor. Default is 2. + sigma : float, optional + Sigma for gaussian filter. Default is `2 * factor / 6.0` which + corresponds to a filter mask twice the size of the scale factor that + covers more than 99% of the gaussian distribution. + order : int, optional + Order of splines used in interpolation of downsampling. See + `scipy.ndimage.map_coordinates` for detail. Default is 1. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + Default is 'reflect'. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. Default is 0. + + Returns + ------- + pyramid : list of arrays + + """ + + _check_factor(factor) + + pyramid = [] + pyramid.append(image) + + layer = 0 + rows = image.shape[0] + cols = image.shape[1] + + # build downsampled images until max_layer is reached or downsampled image + # has size of 1 in one direction + while True: + layer += 1 + + layer_image = pyramid_reduce(pyramid[-1], factor, sigma, order, + mode, cval) + + # image degraded to 1px + if layer_image.ndim == 1: + break + + prev_rows = rows + prev_cols = cols + rows = layer_image.shape[0] + cols = layer_image.shape[1] + + # no change to previous pyramid layer + if prev_rows == rows and prev_cols == cols: + break + + pyramid.append(layer_image) + + if layer == max_layer: + break + + return pyramid + + +def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, + mode='reflect', cval=0): + """Build laplacian pyramid. + + Each layer contains the difference between the downsampled and the + downsampled plus smoothed image. + + Parameters + ---------- + image : array + Input image. + max_layer : int + Number of layers for the pyramid. 0th layer is the original image. + Default is -1 which builds all possible layers. + factor : float, optional + Downscale factor. Default is 2. + sigma : float, optional + Sigma for gaussian filter. Default is `2 * factor / 6.0` which + corresponds to a filter mask twice the size of the scale factor that + covers more than 99% of the gaussian distribution. + order : int, optional + Order of splines used in interpolation of downsampling. See + `scipy.ndimage.map_coordinates` for detail. Default is 1. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + Default is 'reflect'. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. Default is 0. + + Returns + ------- + pyramid : list of arrays + + """ + + _check_factor(factor) + + if sigma is None: + # automatically determine sigma which covers > 99% of distribution + sigma = 2 * factor / 6.0 + + pyramid = [] + pyramid.append(image - _smooth(image, sigma, mode, cval)) + + layer = 0 + rows = image.shape[0] + cols = image.shape[1] + + # build downsampled images until max_layer is reached or downsampled image + # has size of 1 in one direction + while True: + layer += 1 + + rows = pyramid[-1].shape[0] + cols = pyramid[-1].shape[1] + out_rows = math.ceil(rows / float(factor)) + out_cols = math.ceil(cols / float(factor)) + + resized = resize(pyramid[-1], (out_rows, out_cols), order=order, + mode=mode, cval=cval) + layer_image = _smooth(resized, sigma, mode, cval) + + # image degraded to 1px + if layer_image.ndim == 1: + break + + prev_rows = rows + prev_cols = cols + rows = layer_image.shape[0] + cols = layer_image.shape[1] + + # no change to previous pyramid layer + if prev_rows == rows and prev_cols == cols: + break + + pyramid.append(layer_image) + + if layer == max_layer: + break + + return pyramid From b7ac633a5e470f9164b496d53d9f110581ac4814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 5 Sep 2012 15:06:24 +0200 Subject: [PATCH 498/648] Add missing references --- skimage/transform/pyramid.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/skimage/transform/pyramid.py b/skimage/transform/pyramid.py index 1a011fb6..5c0a7905 100644 --- a/skimage/transform/pyramid.py +++ b/skimage/transform/pyramid.py @@ -168,6 +168,10 @@ def build_gaussian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, ------- pyramid : list of arrays + References + ---------- + ..[1] http://web.mit.edu/persci/people/adelson/pub_pdfs/pyramid83.pdf + """ _check_factor(factor) @@ -242,6 +246,10 @@ def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, ------- pyramid : list of arrays + References + ---------- + ..[1] http://web.mit.edu/persci/people/adelson/pub_pdfs/pyramid83.pdf + """ _check_factor(factor) From c57085520967fa92ad97f67ab85fe3f370c3ff84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 5 Sep 2012 15:07:46 +0200 Subject: [PATCH 499/648] Rename file of pyramid functions --- skimage/transform/__init__.py | 4 ++-- skimage/transform/{pyramid.py => pyramids.py} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename skimage/transform/{pyramid.py => pyramids.py} (100%) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index abf31517..fc2b12ef 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -7,5 +7,5 @@ from ._geometric import (warp, warp_coords, estimate_transform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) from ._warps import swirl, homography, resize, rotate -from .pyramid import (pyramid_reduce, pyramid_expand, - build_gaussian_pyramid, build_laplacian_pyramid) +from .pyramids import (pyramid_reduce, pyramid_expand, + build_gaussian_pyramid, build_laplacian_pyramid) diff --git a/skimage/transform/pyramid.py b/skimage/transform/pyramids.py similarity index 100% rename from skimage/transform/pyramid.py rename to skimage/transform/pyramids.py From eaed3dff1e478ee0ba9e1564ab22fc9ec059f166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 5 Sep 2012 15:18:08 +0200 Subject: [PATCH 500/648] Add tests for pyramid functions --- skimage/transform/tests/test_pyramids.py | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 skimage/transform/tests/test_pyramids.py diff --git a/skimage/transform/tests/test_pyramids.py b/skimage/transform/tests/test_pyramids.py new file mode 100644 index 00000000..e10d317e --- /dev/null +++ b/skimage/transform/tests/test_pyramids.py @@ -0,0 +1,41 @@ +from numpy.testing import assert_array_equal, run_module_suite +from skimage import data +from skimage.transform import (pyramid_reduce, pyramid_expand, + build_gaussian_pyramid, build_laplacian_pyramid) + + +image = data.lena() + + +def test_pyramid_reduce(): + rows, cols, dim = image.shape + out = pyramid_reduce(image, factor=2) + assert_array_equal(out.shape, (rows / 2, cols / 2, dim)) + + +def test_pyramid_expand(): + rows, cols, dim = image.shape + out = pyramid_expand(image, factor=2) + assert_array_equal(out.shape, (rows * 2, cols * 2, dim)) + + +def test_build_gaussian_pyramid(): + rows, cols, dim = image.shape + pyramid = build_gaussian_pyramid(image, factor=2) + + for layer, out in enumerate(pyramid): + layer_shape = (rows / 2 ** layer, cols / 2 ** layer, dim) + assert_array_equal(out.shape, layer_shape) + + +def test_build_laplacian_pyramid(): + rows, cols, dim = image.shape + pyramid = build_laplacian_pyramid(image, factor=2) + + for layer, out in enumerate(pyramid): + layer_shape = (rows / 2 ** layer, cols / 2 ** layer, dim) + assert_array_equal(out.shape, layer_shape) + + +if __name__ == "__main__": + run_module_suite() From db5647285f83308830e1018d92ea5bb57ea19c5c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 4 Sep 2012 23:03:16 -0400 Subject: [PATCH 501/648] DOC: Add quickstart for viewer to User Guide --- doc/source/user_guide.txt | 1 + .../user_guide/data/denoise_plugin_window.png | Bin 0 -> 12844 bytes .../user_guide/data/denoise_viewer_window.png | Bin 0 -> 91599 bytes doc/source/user_guide/viewer.txt | 87 ++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 doc/source/user_guide/data/denoise_plugin_window.png create mode 100644 doc/source/user_guide/data/denoise_viewer_window.png create mode 100644 doc/source/user_guide/viewer.txt diff --git a/doc/source/user_guide.txt b/doc/source/user_guide.txt index 01344806..178e130d 100644 --- a/doc/source/user_guide.txt +++ b/doc/source/user_guide.txt @@ -8,3 +8,4 @@ User Guide user_guide/plugins user_guide/tutorials user_guide/getting_help + user_guide/viewer diff --git a/doc/source/user_guide/data/denoise_plugin_window.png b/doc/source/user_guide/data/denoise_plugin_window.png new file mode 100644 index 0000000000000000000000000000000000000000..3c1ff3e3522214a069ad3bc0d0c4c0b2b8916428 GIT binary patch literal 12844 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%0D=;sDzzR+#?Bq+gQpGL5kC!TND~~>_1BXu` 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 MDmuy~iqC@oKLDEQO#lD@ literal 0 HcmV?d00001 diff --git a/doc/source/user_guide/viewer.txt b/doc/source/user_guide/viewer.txt new file mode 100644 index 00000000..f194ade6 --- /dev/null +++ b/doc/source/user_guide/viewer.txt @@ -0,0 +1,87 @@ +Image Viewer +============ + + +Quick Start +----------- + + +``skimage.viewer`` provides a matplotlib_-based canvas for displaying images and +a Qt-based GUI-toolkit, with the goal of making it easy to create interactive +image editors. You can simply use it to display an image:: + + >>> from skimage import data + >>> from skimage.viewer import ImageViewer + + >>> image = data.camera() + >>> view = ImageViewer(image) + >>> view.show() + +Of course, you could just as easily use ``imshow`` from matplotlib_ (or +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:: + + >>> import skimage + >>> from skimage.viewer.plugins import Canny + + >>> image = skimage.img_as_float(data.camera()) + >>> viewer = ImageViewer(image) + >>> viewer += Canny(view) + >>> 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``: + +.. code-block:: python + + from skimage.filter import tv_denoise + from skimage.viewer.plugins.base import Plugin + + denoise_plugin = Plugin(image_filter=tv_denoise) + +.. note:: + + The ``Plugin`` assumes the first argument given to the image filter is the + image from the image viewer. In the future, this should be changed so you + can pass the image to a different argument of the filter function. + +To actually interact with the filter, you have to add widgets that adjust the +parameters of the function. Typically, that means adding a slider widget and +connecting it to the filter parameter and the minimum and maximum values of the +slider: + +.. code-block:: python + + from skimage.viewer.widgets import Slider + from skimage.viewer.widgets.history import SaveButtons + + denoise_plugin += Slider('weight', 0.01, 0.5, update_on='release') + denoise_plugin += SaveButtons() + +Here, we connect a slider widget to the filter's 'weight' argument. We also +added some buttons for saving the image to file or to the ``scikits-image`` +image stack (see ``skimage.io.push`` and ``skimage.io.pop``). + +All that's left is to create an image viewer and add the plugin to that viewer. + +.. code-block:: python + + from skimage import data + from skimage.viewer import ImageViewer + + image = data.coins() + viewer = ImageViewer(image) + viewer += denoise_plugin + viewer.show() + + +.. image:: data/denoise_viewer_window.png +.. image:: data/denoise_plugin_window.png + + +.. _matplotlib: http://matplotlib.sourceforge.net/ + From d98a92a603eced6c14373f8df62a56163d719fab Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 4 Sep 2012 23:04:32 -0400 Subject: [PATCH 502/648] ENH: Set viewer min-intensity to zero when possible. Some image dtypes allow negative values, but these are typically not used. If an dtype allows negative values, but no negative values are present in the image, set the minimum value to 0 so that the image doesn't look washed out. --- skimage/viewer/viewers/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 7c4b4753..490ff92c 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -144,6 +144,8 @@ class ImageViewer(QMainWindow): self._img = image self._image_plot.set_array(image) clim = dtype_range[image.dtype.type] + if clim[0] < 0 and image.min() >= 0: + clim = (0, clim[1]) self._image_plot.set_clim(clim) self.redraw() From 5a632dda3f79db1ecd8c321969ca7b71f0601389 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 4 Sep 2012 23:21:27 -0400 Subject: [PATCH 503/648] STY: Clean up formatting and remove repeated imports --- doc/source/user_guide/viewer.txt | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/doc/source/user_guide/viewer.txt b/doc/source/user_guide/viewer.txt index f194ade6..17387240 100644 --- a/doc/source/user_guide/viewer.txt +++ b/doc/source/user_guide/viewer.txt @@ -8,29 +8,31 @@ Quick Start ``skimage.viewer`` provides a matplotlib_-based canvas for displaying images and a Qt-based GUI-toolkit, with the goal of making it easy to create interactive -image editors. You can simply use it to display an image:: +image editors. You can simply use it to display an image: - >>> from skimage import data - >>> from skimage.viewer import ImageViewer +.. code-block:: python - >>> image = data.camera() - >>> view = ImageViewer(image) - >>> view.show() + from skimage import data + from skimage.viewer import ImageViewer + + image = data.coins() + viewer = ImageViewer(image) + viewer.show() Of course, you could just as easily use ``imshow`` from matplotlib_ (or 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 plugin is added to the viewer: - >>> import skimage - >>> from skimage.viewer.plugins import Canny +.. code-block:: python - >>> image = skimage.img_as_float(data.camera()) - >>> viewer = ImageViewer(image) - >>> viewer += Canny(view) - >>> viewer.show() + from skimage.viewer.plugins import Canny + + viewer = ImageViewer(image) + viewer += Canny(view) + 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 @@ -70,10 +72,6 @@ All that's left is to create an image viewer and add the plugin to that viewer. .. code-block:: python - from skimage import data - from skimage.viewer import ImageViewer - - image = data.coins() viewer = ImageViewer(image) viewer += denoise_plugin viewer.show() From 72623c672d91fc131ae65effa38ca83f3e34ac37 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 5 Sep 2012 07:57:08 -0700 Subject: [PATCH 504/648] TST: Add Travis configuration based on nipy's. --- .travis.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..f2708daa --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +# vim ft=yaml +# travis-ci.org definition for skimage build +# +# We pretend to be erlang because we need can't use the python support in +# travis-ci; it uses virtualenvs, they do not have numpy, scipy, matplotlib, +# and it is impractical to build them +language: erlang +env: + - PYTHON=python PYSUF='' + # - PYTHON=python3 PYSUF=3 : python3-numpy not currently available +install: + # - sudo apt-get build-dep $PYTHON-numpy + - sudo apt-get install $PYTHON-dev + - sudo apt-get install $PYTHON-numpy + - sudo apt-get install $PYTHON-scipy + - sudo apt-get install $PYTHON-setuptools + - sudo apt-get install $PYTHON-nose + - sudo apt-get install cython + - sudo apt-get install libfreeimage3 + - $PYTHON setup.py build + - sudo $PYTHON setup.py install +script: + # Change into an innocuous directory and find tests from installation + - mkdir for_test + - cd for_test + - nosetests `$PYTHON -c "import os; import skimage; print(os.path.dirname(skimage.__file__))"` From 560dac26d63a493cbe297348fe7279e7a525ba15 Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Thu, 6 Sep 2012 10:43:38 -0400 Subject: [PATCH 505/648] FIX: configure just skimage logger on import --- skimage/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index ad37f425..c417c2be 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -123,18 +123,17 @@ def _setup_log(): import logging import sys - log = logging.getLogger() + formatter = logging.Formatter( + '%(name)s: %(levelname)s: %(message)s' + ) try: handler = logging.StreamHandler(stream=sys.stdout) except TypeError: handler = logging.StreamHandler(strm=sys.stdout) - - formatter = logging.Formatter( - '%(name)s: %(levelname)s: %(message)s' - ) handler.setFormatter(formatter) + log = get_log() log.addHandler(handler) log.setLevel(logging.WARNING) From ed8f214a951895158ac1f7583e064a08943e4ef2 Mon Sep 17 00:00:00 2001 From: James Bergstra Date: Thu, 6 Sep 2012 10:57:21 -0400 Subject: [PATCH 506/648] ENH: default logger blocks messages from global handler --- skimage/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/__init__.py b/skimage/__init__.py index c417c2be..c8935cb6 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -136,6 +136,7 @@ def _setup_log(): log = get_log() log.addHandler(handler) log.setLevel(logging.WARNING) + log.propagate = False _setup_log() From c42b3d656dc31f86338d3066c49be276730d06ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 7 Sep 2012 18:11:48 +0200 Subject: [PATCH 507/648] Fix wrong factor in pyramid_expand --- skimage/transform/pyramids.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index 5c0a7905..e5507460 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -121,8 +121,8 @@ def pyramid_expand(image, factor=2, sigma=None, order=1, rows = image.shape[0] cols = image.shape[1] - out_rows = 2 * rows - out_cols = 2 * cols + out_rows = math.ceil(factor * rows) + out_cols = math.ceil(factor * cols) if sigma is None: # automatically determine sigma which covers > 99% of distribution From b7e965eec0a60fff314c488f750a37b808e219ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 7 Sep 2012 18:26:57 +0200 Subject: [PATCH 508/648] Fix singleton dimension for 1 pixel image --- skimage/transform/_geometric.py | 6 ++++-- skimage/transform/pyramids.py | 18 ++---------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index f81e6e22..c3f21db9 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1016,5 +1016,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, if mode == 'constant' and not (0 <= cval <= 1): clipped[out == cval] = cval - # Remove singleton dim introduced by atleast_3d - return clipped.squeeze() + if clipped.shape[0] == 1 or clipped.shape[1] == 1: + return clipped + else: # remove singleton dim introduced by atleast_3d + return clipped.squeeze() diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index e5507460..0fdef92e 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -185,16 +185,12 @@ def build_gaussian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, # build downsampled images until max_layer is reached or downsampled image # has size of 1 in one direction - while True: + while layer != max_layer: layer += 1 layer_image = pyramid_reduce(pyramid[-1], factor, sigma, order, mode, cval) - # image degraded to 1px - if layer_image.ndim == 1: - break - prev_rows = rows prev_cols = cols rows = layer_image.shape[0] @@ -206,9 +202,6 @@ def build_gaussian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, pyramid.append(layer_image) - if layer == max_layer: - break - return pyramid @@ -267,7 +260,7 @@ def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, # build downsampled images until max_layer is reached or downsampled image # has size of 1 in one direction - while True: + while layer != max_layer: layer += 1 rows = pyramid[-1].shape[0] @@ -279,10 +272,6 @@ def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, mode=mode, cval=cval) layer_image = _smooth(resized, sigma, mode, cval) - # image degraded to 1px - if layer_image.ndim == 1: - break - prev_rows = rows prev_cols = cols rows = layer_image.shape[0] @@ -294,7 +283,4 @@ def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, pyramid.append(layer_image) - if layer == max_layer: - break - return pyramid From 2780ae63e2591c64b50a5e8de982000d5fb55147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 7 Sep 2012 18:33:43 +0200 Subject: [PATCH 509/648] Make pyramid functions generators --- skimage/transform/pyramids.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index 0fdef92e..babdea58 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -176,23 +176,24 @@ def build_gaussian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, _check_factor(factor) - pyramid = [] - pyramid.append(image) - layer = 0 rows = image.shape[0] cols = image.shape[1] + prev_layer_image = image + yield prev_layer_image + # build downsampled images until max_layer is reached or downsampled image # has size of 1 in one direction while layer != max_layer: layer += 1 - layer_image = pyramid_reduce(pyramid[-1], factor, sigma, order, + layer_image = pyramid_reduce(prev_layer_image, factor, sigma, order, mode, cval) prev_rows = rows prev_cols = cols + prev_layer_image = layer_image rows = layer_image.shape[0] cols = layer_image.shape[1] @@ -200,9 +201,7 @@ def build_gaussian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, if prev_rows == rows and prev_cols == cols: break - pyramid.append(layer_image) - - return pyramid + yield layer_image def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, @@ -251,29 +250,30 @@ def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, # automatically determine sigma which covers > 99% of distribution sigma = 2 * factor / 6.0 - pyramid = [] - pyramid.append(image - _smooth(image, sigma, mode, cval)) - layer = 0 rows = image.shape[0] cols = image.shape[1] + prev_layer_image = image - _smooth(image, sigma, mode, cval) + yield prev_layer_image + # build downsampled images until max_layer is reached or downsampled image # has size of 1 in one direction while layer != max_layer: layer += 1 - rows = pyramid[-1].shape[0] - cols = pyramid[-1].shape[1] + rows = prev_layer_image.shape[0] + cols = prev_layer_image.shape[1] out_rows = math.ceil(rows / float(factor)) out_cols = math.ceil(cols / float(factor)) - resized = resize(pyramid[-1], (out_rows, out_cols), order=order, + resized = resize(prev_layer_image, (out_rows, out_cols), order=order, mode=mode, cval=cval) layer_image = _smooth(resized, sigma, mode, cval) prev_rows = rows prev_cols = cols + prev_layer_image = layer_image rows = layer_image.shape[0] cols = layer_image.shape[1] @@ -281,6 +281,4 @@ def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, if prev_rows == rows and prev_cols == cols: break - pyramid.append(layer_image) - - return pyramid + yield layer_image From bccbc36b91afcd3baede742a41707bc96bb25b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 7 Sep 2012 18:47:50 +0200 Subject: [PATCH 510/648] Use consistent dtype for all levels of pyramid --- skimage/transform/pyramids.py | 10 ++++++++-- skimage/transform/tests/test_pyramids.py | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index babdea58..bccc5f51 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -119,6 +119,8 @@ def pyramid_expand(image, factor=2, sigma=None, order=1, _check_factor(factor) + image = img_as_float(image) + rows = image.shape[0] cols = image.shape[1] out_rows = math.ceil(factor * rows) @@ -176,12 +178,15 @@ def build_gaussian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, _check_factor(factor) + image = img_as_float(image) + layer = 0 rows = image.shape[0] cols = image.shape[1] + # cast to float for consistent data type in pyramid prev_layer_image = image - yield prev_layer_image + yield image # build downsampled images until max_layer is reached or downsampled image # has size of 1 in one direction @@ -246,6 +251,8 @@ def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, _check_factor(factor) + image = img_as_float(image) + if sigma is None: # automatically determine sigma which covers > 99% of distribution sigma = 2 * factor / 6.0 @@ -255,7 +262,6 @@ def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, cols = image.shape[1] prev_layer_image = image - _smooth(image, sigma, mode, cval) - yield prev_layer_image # build downsampled images until max_layer is reached or downsampled image # has size of 1 in one direction diff --git a/skimage/transform/tests/test_pyramids.py b/skimage/transform/tests/test_pyramids.py index e10d317e..981b93fe 100644 --- a/skimage/transform/tests/test_pyramids.py +++ b/skimage/transform/tests/test_pyramids.py @@ -33,6 +33,7 @@ def test_build_laplacian_pyramid(): pyramid = build_laplacian_pyramid(image, factor=2) for layer, out in enumerate(pyramid): + layer += 1 layer_shape = (rows / 2 ** layer, cols / 2 ** layer, dim) assert_array_equal(out.shape, layer_shape) From f078854197cae61691d86b043b92f0b613409fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 7 Sep 2012 18:57:36 +0200 Subject: [PATCH 511/648] Rename factor parameters for better comprehensibility --- skimage/transform/pyramids.py | 56 ++++++++++++------------ skimage/transform/tests/test_pyramids.py | 8 ++-- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index bccc5f51..470218a5 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -27,7 +27,7 @@ def _check_factor(factor): raise ValueError('scale factor must be greater than 1') -def pyramid_reduce(image, factor=2, sigma=None, order=1, +def pyramid_reduce(image, downscale=2, sigma=None, order=1, mode='reflect', cval=0): """Smooth and then downsample image. @@ -35,10 +35,10 @@ def pyramid_reduce(image, factor=2, sigma=None, order=1, ---------- image : array Input image. - factor : float, optional + downscale : float, optional Downscale factor. Default is 2. sigma : float, optional - Sigma for gaussian filter. Default is `2 * factor / 6.0` which + Sigma for gaussian filter. Default is `2 * downscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that covers more than 99% of the gaussian distribution. order : int, optional @@ -62,18 +62,18 @@ def pyramid_reduce(image, factor=2, sigma=None, order=1, """ - _check_factor(factor) + _check_factor(downscale) image = img_as_float(image) rows = image.shape[0] cols = image.shape[1] - out_rows = math.ceil(rows / float(factor)) - out_cols = math.ceil(cols / float(factor)) + out_rows = math.ceil(rows / float(downscale)) + out_cols = math.ceil(cols / float(downscale)) if sigma is None: # automatically determine sigma which covers > 99% of distribution - sigma = 2 * factor / 6.0 + sigma = 2 * downscale / 6.0 smoothed = _smooth(image, sigma, mode, cval) out = resize(smoothed, (out_rows, out_cols), order=order, @@ -82,7 +82,7 @@ def pyramid_reduce(image, factor=2, sigma=None, order=1, return out -def pyramid_expand(image, factor=2, sigma=None, order=1, +def pyramid_expand(image, upscale=2, sigma=None, order=1, mode='reflect', cval=0): """Upsample and then smooth image. @@ -90,10 +90,10 @@ def pyramid_expand(image, factor=2, sigma=None, order=1, ---------- image : array Input image. - factor : float, optional + upscale : float, optional Upscale factor. Default is 2. sigma : float, optional - Sigma for gaussian filter. Default is `2 * factor / 6.0` which + Sigma for gaussian filter. Default is `2 * upscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that covers more than 99% of the gaussian distribution. order : int, optional @@ -117,18 +117,18 @@ def pyramid_expand(image, factor=2, sigma=None, order=1, """ - _check_factor(factor) + _check_factor(upscale) image = img_as_float(image) rows = image.shape[0] cols = image.shape[1] - out_rows = math.ceil(factor * rows) - out_cols = math.ceil(factor * cols) + out_rows = math.ceil(upscale * rows) + out_cols = math.ceil(upscale * cols) if sigma is None: # automatically determine sigma which covers > 99% of distribution - sigma = 2 * factor / 6.0 + sigma = 2 * upscale / 6.0 resized = resize(image, (out_rows, out_cols), order=order, mode=mode, cval=cval) @@ -137,8 +137,8 @@ def pyramid_expand(image, factor=2, sigma=None, order=1, return out -def build_gaussian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, - mode='reflect', cval=0): +def build_gaussian_pyramid(image, max_layer=-1, downscale=2, sigma=None, + order=1, mode='reflect', cval=0): """Build gaussian pyramid. Recursively applies the `pyramid_reduce` function to the image. @@ -150,10 +150,10 @@ def build_gaussian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, max_layer : int Number of layers for the pyramid. 0th layer is the original image. Default is -1 which builds all possible layers. - factor : float, optional + downscale : float, optional Downscale factor. Default is 2. sigma : float, optional - Sigma for gaussian filter. Default is `2 * factor / 6.0` which + Sigma for gaussian filter. Default is `2 * downscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that covers more than 99% of the gaussian distribution. order : int, optional @@ -176,7 +176,7 @@ def build_gaussian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, """ - _check_factor(factor) + _check_factor(downscale) image = img_as_float(image) @@ -193,7 +193,7 @@ def build_gaussian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, while layer != max_layer: layer += 1 - layer_image = pyramid_reduce(prev_layer_image, factor, sigma, order, + layer_image = pyramid_reduce(prev_layer_image, downscale, sigma, order, mode, cval) prev_rows = rows @@ -209,8 +209,8 @@ def build_gaussian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, yield layer_image -def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, - mode='reflect', cval=0): +def build_laplacian_pyramid(image, max_layer=-1, downscale=2, sigma=None, + order=1, mode='reflect', cval=0): """Build laplacian pyramid. Each layer contains the difference between the downsampled and the @@ -223,10 +223,10 @@ def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, max_layer : int Number of layers for the pyramid. 0th layer is the original image. Default is -1 which builds all possible layers. - factor : float, optional + downscale : float, optional Downscale factor. Default is 2. sigma : float, optional - Sigma for gaussian filter. Default is `2 * factor / 6.0` which + Sigma for gaussian filter. Default is `2 * downscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that covers more than 99% of the gaussian distribution. order : int, optional @@ -249,13 +249,13 @@ def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, """ - _check_factor(factor) + _check_factor(downscale) image = img_as_float(image) if sigma is None: # automatically determine sigma which covers > 99% of distribution - sigma = 2 * factor / 6.0 + sigma = 2 * downscale / 6.0 layer = 0 rows = image.shape[0] @@ -270,8 +270,8 @@ def build_laplacian_pyramid(image, max_layer=-1, factor=2, sigma=None, order=1, rows = prev_layer_image.shape[0] cols = prev_layer_image.shape[1] - out_rows = math.ceil(rows / float(factor)) - out_cols = math.ceil(cols / float(factor)) + out_rows = math.ceil(rows / float(downscale)) + out_cols = math.ceil(cols / float(downscale)) resized = resize(prev_layer_image, (out_rows, out_cols), order=order, mode=mode, cval=cval) diff --git a/skimage/transform/tests/test_pyramids.py b/skimage/transform/tests/test_pyramids.py index 981b93fe..d9e6c599 100644 --- a/skimage/transform/tests/test_pyramids.py +++ b/skimage/transform/tests/test_pyramids.py @@ -9,19 +9,19 @@ image = data.lena() def test_pyramid_reduce(): rows, cols, dim = image.shape - out = pyramid_reduce(image, factor=2) + out = pyramid_reduce(image, downscale=2) assert_array_equal(out.shape, (rows / 2, cols / 2, dim)) def test_pyramid_expand(): rows, cols, dim = image.shape - out = pyramid_expand(image, factor=2) + out = pyramid_expand(image, upscale=2) assert_array_equal(out.shape, (rows * 2, cols * 2, dim)) def test_build_gaussian_pyramid(): rows, cols, dim = image.shape - pyramid = build_gaussian_pyramid(image, factor=2) + pyramid = build_gaussian_pyramid(image, downscale=2) for layer, out in enumerate(pyramid): layer_shape = (rows / 2 ** layer, cols / 2 ** layer, dim) @@ -30,7 +30,7 @@ def test_build_gaussian_pyramid(): def test_build_laplacian_pyramid(): rows, cols, dim = image.shape - pyramid = build_laplacian_pyramid(image, factor=2) + pyramid = build_laplacian_pyramid(image, downscale=2) for layer, out in enumerate(pyramid): layer += 1 From e6675dcda6721acb6ad70adc68f1e33ce8190fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 7 Sep 2012 19:07:58 +0200 Subject: [PATCH 512/648] Add example script for image pyramids --- doc/examples/plot_pyramid.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 doc/examples/plot_pyramid.py diff --git a/doc/examples/plot_pyramid.py b/doc/examples/plot_pyramid.py new file mode 100644 index 00000000..cacf0689 --- /dev/null +++ b/doc/examples/plot_pyramid.py @@ -0,0 +1,32 @@ +""" +==================== +Build image pyramids +==================== + +This example shows how to build image pyramids. +""" + +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data +from skimage import img_as_float +from skimage.transform import build_gaussian_pyramid + + +image = data.lena() +rows, cols, dim = image.shape +pyramid = tuple(build_gaussian_pyramid(image, downscale=2)) + +display = np.zeros((rows, cols + cols / 2, 3), dtype=np.double) + +display[:rows, :cols, :] = pyramid[0] + +i_row = 0 +for p in pyramid[1:]: + n_rows, n_cols = p.shape[:2] + display[i_row:i_row + n_rows, cols:cols + n_cols] = p + i_row += n_rows + +plt.imshow(display) +plt.show() From 8e14f5f073dc054933f744978699b440213e3b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 8 Sep 2012 15:24:14 +0200 Subject: [PATCH 513/648] Add missing 0th layer of laplacian pyramid --- skimage/transform/pyramids.py | 1 + skimage/transform/tests/test_pyramids.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index 470218a5..4dd6f7b1 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -262,6 +262,7 @@ def build_laplacian_pyramid(image, max_layer=-1, downscale=2, sigma=None, cols = image.shape[1] prev_layer_image = image - _smooth(image, sigma, mode, cval) + yield prev_layer_image # build downsampled images until max_layer is reached or downsampled image # has size of 1 in one direction diff --git a/skimage/transform/tests/test_pyramids.py b/skimage/transform/tests/test_pyramids.py index d9e6c599..9aa236ff 100644 --- a/skimage/transform/tests/test_pyramids.py +++ b/skimage/transform/tests/test_pyramids.py @@ -33,7 +33,6 @@ def test_build_laplacian_pyramid(): pyramid = build_laplacian_pyramid(image, downscale=2) for layer, out in enumerate(pyramid): - layer += 1 layer_shape = (rows / 2 ** layer, cols / 2 ** layer, dim) assert_array_equal(out.shape, layer_shape) From 6142df6531d294c530bf12464e15f6d50ff33962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 8 Sep 2012 17:24:11 +0200 Subject: [PATCH 514/648] Update doc string return type of image pyramids --- skimage/transform/pyramids.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index 4dd6f7b1..7a8e8228 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -168,7 +168,8 @@ def build_gaussian_pyramid(image, max_layer=-1, downscale=2, sigma=None, Returns ------- - pyramid : list of arrays + pyramid : generator + Generator yielding pyramid layers. References ---------- @@ -241,7 +242,8 @@ def build_laplacian_pyramid(image, max_layer=-1, downscale=2, sigma=None, Returns ------- - pyramid : list of arrays + pyramid : generator + Generator yielding pyramid layers. References ---------- From c477c7b2ec5de8c2776a7b3b99ebfeae073f739b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 8 Sep 2012 18:23:40 +0200 Subject: [PATCH 515/648] Remove unnecessary C-contiguous flag --- skimage/measure/_find_contours.pyx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/measure/_find_contours.pyx b/skimage/measure/_find_contours.pyx index 17702f95..4f1b3cee 100644 --- a/skimage/measure/_find_contours.pyx +++ b/skimage/measure/_find_contours.pyx @@ -6,14 +6,14 @@ cimport numpy as np np.import_array() -cdef inline double _get_fraction(double from_value, double to_value, +cdef inline double _get_fraction(double from_value, double to_value, double level): if (to_value == from_value): return 0 return ((level - from_value) / (to_value - from_value)) -def iterate_and_store(np.ndarray[double, ndim=2, mode='c'] array, +def iterate_and_store(np.ndarray[double, ndim=2] array, double level, int vertex_connect_high): """Iterate across the given array in a marching-squares fashion, looking for segments that cross 'level'. If such a segment is @@ -92,7 +92,7 @@ def iterate_and_store(np.ndarray[double, ndim=2, mode='c'] array, else: coords[0] += 1 coords[1] = 0 - + square_case = 0 if (ul > level): square_case += 1 @@ -103,7 +103,7 @@ def iterate_and_store(np.ndarray[double, ndim=2, mode='c'] array, if (square_case != 0 and square_case != 15): # only do anything if there's a line passing through the # square. Cases 0 and 15 are entirely below/above the contour. - + top = r0, c0 + _get_fraction(ul, ur, level) bottom = r1, c0 + _get_fraction(ll, lr, level) left = r0 + _get_fraction(ul, ll, level), c0 From 757c4972b7588a9e1dbbf6869ca33c43fee1b57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 8 Sep 2012 18:28:32 +0200 Subject: [PATCH 516/648] Add test case for memory order --- skimage/measure/tests/test_find_contours.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/skimage/measure/tests/test_find_contours.py b/skimage/measure/tests/test_find_contours.py index fbc502f7..62b39b3f 100644 --- a/skimage/measure/tests/test_find_contours.py +++ b/skimage/measure/tests/test_find_contours.py @@ -62,6 +62,14 @@ def test_float(): [ 2., 3.]]) +def test_memory_order(): + contours = find_contours(np.ascontiguousarray(r), 0.5) + assert len(contours) == 1 + + contours = find_contours(np.asfortranarray(r), 0.5) + assert len(contours) == 1 + + if __name__ == '__main__': from numpy.testing import run_module_suite run_module_suite() From cc5533b17a8f040df3049c0fbd2149025f23d802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 8 Sep 2012 18:49:21 +0200 Subject: [PATCH 517/648] Add dependencies to setup process --- setup.py | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/setup.py b/setup.py index efa5f8db..40ad846f 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ try: except ImportError: from distutils.command.build_py import build_py + def configuration(parent_package='', top_path=None): if os.path.exists('MANIFEST'): os.remove('MANIFEST') @@ -44,6 +45,7 @@ def configuration(parent_package='', top_path=None): return config + def write_version_py(filename='skimage/version.py'): template = """# THIS FILE IS GENERATED FROM THE SKIMAGE SETUP.PY version='%s' @@ -57,6 +59,7 @@ version='%s' finally: vfile.close() + if __name__ == "__main__": write_version_py() @@ -72,23 +75,32 @@ if __name__ == "__main__": version=VERSION, classifiers = - [ 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: C', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Topic :: Scientific/Engineering', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Operating System :: Unix', - 'Operating System :: MacOS', - ], + ['Development Status :: 4 - Beta', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: C', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Topic :: Scientific/Engineering', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: Unix', + 'Operating System :: MacOS', + ], configuration=configuration, - install_requires=[], + install_requires= + ['Python >= 2.5', + 'Numpy >= 1.6', + 'Cython >= 0.15', + 'SciPy >= 0.10', + ], + extras_require= + {'Viewer': ['PyQt', 'matplotlib'], + 'Tests': ['nose'], + }, packages=setuptools.find_packages(), include_package_data=True, zip_safe=False, # the package can run out of an .egg file From ab58ed0ce1e5dd3b2a3b60fb199362d566e40591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 8 Sep 2012 20:02:53 +0200 Subject: [PATCH 518/648] Improve code layout --- setup.py | 60 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/setup.py b/setup.py index 40ad846f..48ea48a8 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #! /usr/bin/env python -descr = """Image Processing SciKit +descr = """Image Processing SciKit Image processing algorithms for SciPy, including IO, morphology, filtering, warping, color manipulation, object detection, etc. @@ -74,41 +74,43 @@ if __name__ == "__main__": download_url=DOWNLOAD_URL, version=VERSION, - classifiers = - ['Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: C', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Topic :: Scientific/Engineering', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Operating System :: Unix', - 'Operating System :: MacOS', - ], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: C', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Topic :: Scientific/Engineering', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: Unix', + 'Operating System :: MacOS', + ], configuration=configuration, - install_requires= - ['Python >= 2.5', - 'Numpy >= 1.6', - 'Cython >= 0.15', - 'SciPy >= 0.10', - ], - extras_require= - {'Viewer': ['PyQt', 'matplotlib'], - 'Tests': ['nose'], - }, + + install_requires=[ + 'Python >= 2.5', + 'Numpy >= 1.6', + 'Cython >= 0.15', + 'SciPy >= 0.10', + ], + + extras_require={ + 'Viewer': ['PyQt', 'matplotlib'], + 'Tests': ['nose'], + }, + packages=setuptools.find_packages(), include_package_data=True, zip_safe=False, # the package can run out of an .egg file entry_points={ - 'console_scripts': [ - 'skivi = skimage.scripts.skivi:main'] - }, + 'console_scripts': ['skivi = skimage.scripts.skivi:main'], + }, cmdclass={'build_py': build_py}, ) From 38f0ca3dbc7d30d7d1d0c1ab0f25ed0b51a1c4ce Mon Sep 17 00:00:00 2001 From: kuantkid Date: Sun, 9 Sep 2012 23:19:29 +0800 Subject: [PATCH 519/648] FIX convert to (unsigned) integer from bool type --- skimage/util/dtype.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index 0ca3197b..ce5acfc1 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -150,8 +150,17 @@ def convert(image, dtype, force_copy=False, uniform=False): itemsize = dtypeobj.itemsize itemsize_in = dtypeobj_in.itemsize - if kind == 'b' or kind_in == 'b': + if kind == 'b': + # to binary image + prec_loss() return dtype(image) + + if kind_in == 'b': + # from binary image, to float and to integer + if kind == 'f': + return dtype(image) + elif kind in 'ui': + return dtype(image) * dtype_range[dtype][1] if kind in 'ui': imin = np.iinfo(dtype).min From f2a256ec9b983218b8eefc682b87db99bb977cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 19:35:39 +0200 Subject: [PATCH 520/648] Implement own dependency checking before installation --- setup.py | 60 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/setup.py b/setup.py index 48ea48a8..396d8429 100644 --- a/setup.py +++ b/setup.py @@ -18,8 +18,16 @@ URL = 'http://scikits-image.org' LICENSE = 'Modified BSD' DOWNLOAD_URL = 'http://github.com/scikits-image/scikits-image' VERSION = '0.7dev' +PYTHON_VERSION = (2, 5) +DEPENDENCIES = { + 'numpy': (1, 6), + 'scipy': (0, 10), + 'Cython': (0, 15), + } + import os +import sys import setuptools from numpy.distutils.core import setup try: @@ -60,7 +68,45 @@ version='%s' vfile.close() +def get_package_version(package): + version = [] + for version_attr in ('version', 'VERSION', '__version__'): + if hasattr(package, version_attr) \ + and isinstance(getattr(package, version_attr), str): + version_info = getattr(package, version_attr) + for part in version_info.split('.'): + try: + version.append(int(part)) + except ValueError: + pass + return tuple(version) + + +def check_requirements(): + if sys.version_info < PYTHON_VERSION: + raise SystemExit('You need Python version %d.%d or later.' \ + % PYTHON_VERSION) + + for package_name, min_version in DEPENDENCIES.items(): + dep_error = False + try: + package = __import__(package_name) + except ImportError: + dep_error = True + else: + package_version = get_package_version(package) + if min_version > package_version: + dep_error = True + + if dep_error: + raise ImportError('You need `%s` version %d.%d or later.' \ + % ((package_name, ) + min_version)) + + if __name__ == "__main__": + + check_requirements() + write_version_py() setup( @@ -92,18 +138,6 @@ if __name__ == "__main__": configuration=configuration, - install_requires=[ - 'Python >= 2.5', - 'Numpy >= 1.6', - 'Cython >= 0.15', - 'SciPy >= 0.10', - ], - - extras_require={ - 'Viewer': ['PyQt', 'matplotlib'], - 'Tests': ['nose'], - }, - packages=setuptools.find_packages(), include_package_data=True, zip_safe=False, # the package can run out of an .egg file @@ -113,4 +147,4 @@ if __name__ == "__main__": }, cmdclass={'build_py': build_py}, - ) + ) From 2206681a89346970b71ca8d0ed1ff60a861b2ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 19:40:28 +0200 Subject: [PATCH 521/648] Update pyramid example with longer description --- doc/examples/plot_pyramid.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/examples/plot_pyramid.py b/doc/examples/plot_pyramid.py index cacf0689..218e86b1 100644 --- a/doc/examples/plot_pyramid.py +++ b/doc/examples/plot_pyramid.py @@ -3,7 +3,10 @@ Build image pyramids ==================== -This example shows how to build image pyramids. +The `build_gauassian_pyramid` function takes an image and yields successive +images shrunk by a constant scale factor. Image pyramids are often used, e.g., +to implement algorithms for denoising, texture discrimination, and scale- +invariant detection. """ import numpy as np From bab2f715dda1427bf9f17f8b3f009157fce89376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 19:41:06 +0200 Subject: [PATCH 522/648] Use more readable variable name for image --- doc/examples/plot_pyramid.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/examples/plot_pyramid.py b/doc/examples/plot_pyramid.py index 218e86b1..999435d5 100644 --- a/doc/examples/plot_pyramid.py +++ b/doc/examples/plot_pyramid.py @@ -21,15 +21,15 @@ image = data.lena() rows, cols, dim = image.shape pyramid = tuple(build_gaussian_pyramid(image, downscale=2)) -display = np.zeros((rows, cols + cols / 2, 3), dtype=np.double) +composite_image = np.zeros((rows, cols + cols / 2, 3), dtype=np.double) -display[:rows, :cols, :] = pyramid[0] +composite_image[:rows, :cols, :] = pyramid[0] i_row = 0 for p in pyramid[1:]: n_rows, n_cols = p.shape[:2] - display[i_row:i_row + n_rows, cols:cols + n_cols] = p + composite_image[i_row:i_row + n_rows, cols:cols + n_cols] = p i_row += n_rows -plt.imshow(display) +plt.imshow(composite_image) plt.show() From c9fd1f3dd044af88d2e62ed6ca41b6a29f321ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 19:44:13 +0200 Subject: [PATCH 523/648] Add doc string to helper function _smooth --- skimage/transform/pyramids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index 7a8e8228..458b8c20 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -6,8 +6,8 @@ from skimage.util import img_as_float def _smooth(image, sigma, mode, cval): + """Return image with each channel smoothed by the gaussian filter.""" - # allocate output array smoothed = np.empty(image.shape, dtype=np.double) if image.ndim == 3: # apply gaussian filter to all dimensions independently From b812faf3698d2720a2a9e43aaf360a60b13c833e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 19:49:04 +0200 Subject: [PATCH 524/648] Remove default parameter values from doc string --- skimage/transform/pyramids.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index 458b8c20..53ec614c 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -36,20 +36,19 @@ def pyramid_reduce(image, downscale=2, sigma=None, order=1, image : array Input image. downscale : float, optional - Downscale factor. Default is 2. + Downscale factor. sigma : float, optional Sigma for gaussian filter. Default is `2 * downscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that covers more than 99% of the gaussian distribution. order : int, optional Order of splines used in interpolation of downsampling. See - `scipy.ndimage.map_coordinates` for detail. Default is 1. + `scipy.ndimage.map_coordinates` for detail. mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where cval is the value when mode is equal to 'constant'. - Default is 'reflect'. cval : float, optional - Value to fill past edges of input if mode is 'constant'. Default is 0. + Value to fill past edges of input if mode is 'constant'. Returns ------- @@ -91,20 +90,19 @@ def pyramid_expand(image, upscale=2, sigma=None, order=1, image : array Input image. upscale : float, optional - Upscale factor. Default is 2. + Upscale factor. sigma : float, optional Sigma for gaussian filter. Default is `2 * upscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that covers more than 99% of the gaussian distribution. order : int, optional Order of splines used in interpolation of downsampling. See - `scipy.ndimage.map_coordinates` for detail. Default is 1. + `scipy.ndimage.map_coordinates` for detail. mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where cval is the value when mode is equal to 'constant'. - Default is 'reflect'. cval : float, optional - Value to fill past edges of input if mode is 'constant'. Default is 0. + Value to fill past edges of input if mode is 'constant'. Returns ------- @@ -151,20 +149,19 @@ def build_gaussian_pyramid(image, max_layer=-1, downscale=2, sigma=None, Number of layers for the pyramid. 0th layer is the original image. Default is -1 which builds all possible layers. downscale : float, optional - Downscale factor. Default is 2. + Downscale factor. sigma : float, optional Sigma for gaussian filter. Default is `2 * downscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that covers more than 99% of the gaussian distribution. order : int, optional Order of splines used in interpolation of downsampling. See - `scipy.ndimage.map_coordinates` for detail. Default is 1. + `scipy.ndimage.map_coordinates` for detail. mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where cval is the value when mode is equal to 'constant'. - Default is 'reflect'. cval : float, optional - Value to fill past edges of input if mode is 'constant'. Default is 0. + Value to fill past edges of input if mode is 'constant'. Returns ------- @@ -225,20 +222,19 @@ def build_laplacian_pyramid(image, max_layer=-1, downscale=2, sigma=None, Number of layers for the pyramid. 0th layer is the original image. Default is -1 which builds all possible layers. downscale : float, optional - Downscale factor. Default is 2. + Downscale factor. sigma : float, optional Sigma for gaussian filter. Default is `2 * downscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that covers more than 99% of the gaussian distribution. order : int, optional Order of splines used in interpolation of downsampling. See - `scipy.ndimage.map_coordinates` for detail. Default is 1. + `scipy.ndimage.map_coordinates` for detail. mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where cval is the value when mode is equal to 'constant'. - Default is 'reflect'. cval : float, optional - Value to fill past edges of input if mode is 'constant'. Default is 0. + Value to fill past edges of input if mode is 'constant'. Returns ------- From f011a816e58ed2c0b0ce2b6c15f793ed692c4547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 19:50:59 +0200 Subject: [PATCH 525/648] Update short description of pyramid functions --- skimage/transform/pyramids.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index 53ec614c..dc35db7b 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -96,7 +96,7 @@ def pyramid_expand(image, upscale=2, sigma=None, order=1, corresponds to a filter mask twice the size of the scale factor that covers more than 99% of the gaussian distribution. order : int, optional - Order of splines used in interpolation of downsampling. See + Order of splines used in interpolation of upsampling. See `scipy.ndimage.map_coordinates` for detail. mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where @@ -137,7 +137,7 @@ def pyramid_expand(image, upscale=2, sigma=None, order=1, def build_gaussian_pyramid(image, max_layer=-1, downscale=2, sigma=None, order=1, mode='reflect', cval=0): - """Build gaussian pyramid. + """Yield images of the gaussian pyramid formed by the input image. Recursively applies the `pyramid_reduce` function to the image. @@ -209,7 +209,7 @@ def build_gaussian_pyramid(image, max_layer=-1, downscale=2, sigma=None, def build_laplacian_pyramid(image, max_layer=-1, downscale=2, sigma=None, order=1, mode='reflect', cval=0): - """Build laplacian pyramid. + """Yield images of the laplacian pyramid formed by the input image. Each layer contains the difference between the downsampled and the downsampled plus smoothed image. From c86d9196d8a631a388c39d0ab2a2e7197990106a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 19:55:46 +0200 Subject: [PATCH 526/648] Add information about return dtype --- skimage/transform/pyramids.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index dc35db7b..b6442bc6 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -53,7 +53,7 @@ def pyramid_reduce(image, downscale=2, sigma=None, order=1, Returns ------- out : array - Smoothed and downsampled image. + Smoothed and downsampled float image. References ---------- @@ -107,7 +107,7 @@ def pyramid_expand(image, upscale=2, sigma=None, order=1, Returns ------- out : array - Upsampled and smoothed image. + Upsampled and smoothed float image. References ---------- @@ -139,7 +139,9 @@ def build_gaussian_pyramid(image, max_layer=-1, downscale=2, sigma=None, order=1, mode='reflect', cval=0): """Yield images of the gaussian pyramid formed by the input image. - Recursively applies the `pyramid_reduce` function to the image. + Recursively applies the `pyramid_reduce` function to the image, and yields + the downscaled images. Note that the first image of the pyramid will be the + original, unscaled image. Parameters ---------- @@ -166,7 +168,7 @@ def build_gaussian_pyramid(image, max_layer=-1, downscale=2, sigma=None, Returns ------- pyramid : generator - Generator yielding pyramid layers. + Generator yielding pyramid layers as float images. References ---------- @@ -239,7 +241,7 @@ def build_laplacian_pyramid(image, max_layer=-1, downscale=2, sigma=None, Returns ------- pyramid : generator - Generator yielding pyramid layers. + Generator yielding pyramid layers as float images. References ---------- From 43afda1fa0ae2d0011d6b87b5c05e3eb1fe13a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 20:02:25 +0200 Subject: [PATCH 527/648] Use gaussian pyramid function for collection viewer example --- viewer_examples/viewers/collection_viewer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/viewer_examples/viewers/collection_viewer.py b/viewer_examples/viewers/collection_viewer.py index b36ddf82..32c0b836 100644 --- a/viewer_examples/viewers/collection_viewer.py +++ b/viewer_examples/viewers/collection_viewer.py @@ -21,9 +21,11 @@ home/end keys import numpy as np from skimage import data from skimage.viewer import CollectionViewer +from skimage.transform import build_gaussian_pyramid + img = data.lena() -img_collection = [np.uint8(img * 0.9**i) for i in range(20)] +img_collection = tuple(build_gaussian_pyramid(img)) view = CollectionViewer(img_collection) view.show() From 8a75cc4626bd38faeec102aea894d4e7ac08646c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 21:01:11 +0200 Subject: [PATCH 528/648] Update description of collection viewer example --- viewer_examples/viewers/collection_viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/viewer_examples/viewers/collection_viewer.py b/viewer_examples/viewers/collection_viewer.py index 32c0b836..24c97ab0 100644 --- a/viewer_examples/viewers/collection_viewer.py +++ b/viewer_examples/viewers/collection_viewer.py @@ -4,7 +4,7 @@ CollectionViewer demo ===================== Demo of CollectionViewer for viewing collections of images. This demo uses -successively darker versions of the same image to fake an image collection. +the different layers of the gaussian pyramid as image collection. You can scroll through images with the slider, or you can interact with the viewer using your keyboard: From 0f175a86b2d3b957adae12c15d1cf8157e5eaaeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 21:34:42 +0200 Subject: [PATCH 529/648] Fix bug in laplacian pyramid caused by renamed variables --- skimage/transform/pyramids.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index b6442bc6..bcd52352 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -261,31 +261,29 @@ def build_laplacian_pyramid(image, max_layer=-1, downscale=2, sigma=None, rows = image.shape[0] cols = image.shape[1] - prev_layer_image = image - _smooth(image, sigma, mode, cval) - yield prev_layer_image + smoothed_image = _smooth(image, sigma, mode, cval) + yield image - smoothed_image # build downsampled images until max_layer is reached or downsampled image # has size of 1 in one direction while layer != max_layer: layer += 1 - rows = prev_layer_image.shape[0] - cols = prev_layer_image.shape[1] + out_rows = math.ceil(rows / float(downscale)) out_cols = math.ceil(cols / float(downscale)) - resized = resize(prev_layer_image, (out_rows, out_cols), order=order, - mode=mode, cval=cval) - layer_image = _smooth(resized, sigma, mode, cval) + resized_image = resize(smoothed_image, (out_rows, out_cols), + order=order, mode=mode, cval=cval) + smoothed_image = _smooth(resized_image, sigma, mode, cval) prev_rows = rows prev_cols = cols - prev_layer_image = layer_image - rows = layer_image.shape[0] - cols = layer_image.shape[1] + rows = resized_image.shape[0] + cols = resized_image.shape[1] # no change to previous pyramid layer if prev_rows == rows and prev_cols == cols: break - yield layer_image + yield resized_image - smoothed_image From 35ef2706065eb80258648c06ff2d4c6476002f90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 21:36:21 +0200 Subject: [PATCH 530/648] Update legacy comment --- skimage/transform/pyramids.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index bcd52352..5028c034 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -188,8 +188,8 @@ def build_gaussian_pyramid(image, max_layer=-1, downscale=2, sigma=None, prev_layer_image = image yield image - # build downsampled images until max_layer is reached or downsampled image - # has size of 1 in one direction + # build downsampled images until max_layer is reached or downscale process + # does not change image size while layer != max_layer: layer += 1 @@ -264,12 +264,11 @@ def build_laplacian_pyramid(image, max_layer=-1, downscale=2, sigma=None, smoothed_image = _smooth(image, sigma, mode, cval) yield image - smoothed_image - # build downsampled images until max_layer is reached or downsampled image - # has size of 1 in one direction + # build downsampled images until max_layer is reached or downscale process + # does not change image size while layer != max_layer: layer += 1 - out_rows = math.ceil(rows / float(downscale)) out_cols = math.ceil(cols / float(downscale)) From d922c962e370b7b65dfbe84ccbcf452ce8c3f044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 21:36:56 +0200 Subject: [PATCH 531/648] Add another reference to laplacian pyramid --- skimage/transform/pyramids.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index 5028c034..79e00466 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -246,6 +246,7 @@ def build_laplacian_pyramid(image, max_layer=-1, downscale=2, sigma=None, References ---------- ..[1] http://web.mit.edu/persci/people/adelson/pub_pdfs/pyramid83.pdf + ..[2] http://sepwww.stanford.edu/~morgan/texturematch/paper_html/node3.html """ From a080624c7d1ca07fd2ddd6125fd24e8aba2e0a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 22:14:42 +0200 Subject: [PATCH 532/648] Move comment to correct code --- skimage/transform/pyramids.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index 79e00466..f07fb1c5 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -178,13 +178,13 @@ def build_gaussian_pyramid(image, max_layer=-1, downscale=2, sigma=None, _check_factor(downscale) + # cast to float for consistent data type in pyramid image = img_as_float(image) layer = 0 rows = image.shape[0] cols = image.shape[1] - # cast to float for consistent data type in pyramid prev_layer_image = image yield image @@ -252,6 +252,7 @@ def build_laplacian_pyramid(image, max_layer=-1, downscale=2, sigma=None, _check_factor(downscale) + # cast to float for consistent data type in pyramid image = img_as_float(image) if sigma is None: From 7766838d78288885f2b2e9a3ea86f1860afcafde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 9 Sep 2012 22:15:15 +0200 Subject: [PATCH 533/648] Remove unused import --- doc/examples/plot_pyramid.py | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/examples/plot_pyramid.py b/doc/examples/plot_pyramid.py index 999435d5..60067549 100644 --- a/doc/examples/plot_pyramid.py +++ b/doc/examples/plot_pyramid.py @@ -13,7 +13,6 @@ import numpy as np import matplotlib.pyplot as plt from skimage import data -from skimage import img_as_float from skimage.transform import build_gaussian_pyramid From c3b5cd1023320f21220cca2339c5f5618b97116b Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 10 Sep 2012 00:25:52 -0400 Subject: [PATCH 534/648] DOC: Add note about negative dtypes --- doc/source/user_guide/data_types.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/doc/source/user_guide/data_types.txt b/doc/source/user_guide/data_types.txt index 1db55543..76d04862 100644 --- a/doc/source/user_guide/data_types.txt +++ b/doc/source/user_guide/data_types.txt @@ -142,6 +142,24 @@ By default, ``rescale_intensity`` stretches the values of ``in_range`` to match the range of the dtype. +Note about negative values +========================== + +People very often represent images in signed dtypes, even though they only +manipulate the positive values of the image (e.g., using only 0-127 in an int8 +image). For this reason, conversion functions *only spread the positive values* +of a signed dtype over the entire range of an unsigned dtype. In other words, +negative values are clipped to 0 when converting from signed to unsigned +dtypes. (Negative values are preserved when converting between signed dtypes.) +To prevent this clipping behavior, you should rescale your image beforehand:: + + >>> image = exposure.rescale_intensity(img_int32, out_range=(0, 2**31 - 1)) + >>> img_uint8 = img_as_ubyte(image) + +This behavior is symmetric: The values in an unsigned dtype are spread over +just the positive range of a signed dtype. + + References ========== From 2c55732fba6dc885ec02eacd9aa8d484fd69462a Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 10 Sep 2012 00:27:30 -0400 Subject: [PATCH 535/648] DOC: float dtype changed from (0, 1) to (-1, 1). The dtype range was changed long ago, but this doc had not been updated to match this behavior. --- doc/source/user_guide/data_types.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/user_guide/data_types.txt b/doc/source/user_guide/data_types.txt index 76d04862..9801c244 100644 --- a/doc/source/user_guide/data_types.txt +++ b/doc/source/user_guide/data_types.txt @@ -14,13 +14,13 @@ Data type Range uint8 0 to 255 uint16 0 to 65535 uint32 0 to 2\ :sup:`32` -float 0 to 1 +float -1 to 1 int8 -128 to 127 int16 -32768 to 32767 int32 -2\ :sup:`31` to 2\ :sup:`31` - 1 ========= ================================= -Note that float images are restricted to the range 0 to 1 even though the data +Note that float images are restricted to the range -1 to 1 even though the data type itself can exceed this range; all integer dtypes, on the other hand, have pixel intensities that can span the entire data type range. Currently, *64-bit (u)int images are not supported*. From 61be60cacec1c4fdee513d704e84d60251d743e1 Mon Sep 17 00:00:00 2001 From: kuantkid Date: Mon, 10 Sep 2012 12:51:50 +0800 Subject: [PATCH 536/648] FIX: early fix for dtype conversion from bool to other type PR #306, Issue #263 --- skimage/util/dtype.py | 5 +---- skimage/util/tests/test_dtype.py | 10 ++++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index ce5acfc1..b44bebbc 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -157,10 +157,7 @@ def convert(image, dtype, force_copy=False, uniform=False): if kind_in == 'b': # from binary image, to float and to integer - if kind == 'f': - return dtype(image) - elif kind in 'ui': - return dtype(image) * dtype_range[dtype][1] + return dtype(image) * dtype_range[dtype][1] if kind in 'ui': imin = np.iinfo(dtype).min diff --git a/skimage/util/tests/test_dtype.py b/skimage/util/tests/test_dtype.py index 9803e0af..946a09eb 100644 --- a/skimage/util/tests/test_dtype.py +++ b/skimage/util/tests/test_dtype.py @@ -93,12 +93,14 @@ def test_bool(): img_[1, 1] = True img8[1, 1] = True funcs = (img_as_float, img_as_int, img_as_ubyte, img_as_uint, img_as_bool) - for func in funcs: + for (func, dt) in [(img_as_int, np.int16), + (img_as_float, np.float64), + (img_as_uint, np.uint16), + (img_as_ubyte, np.ubyte)]: converted_ = func(img_) - assert np.sum(converted_) == 1 + assert np.sum(converted_) == dtype_range[dt][1] converted8 = func(img8) - assert np.sum(converted8) == 1 - + assert np.sum(converted8) == dtype_range[dt][1] if __name__ == '__main__': np.testing.run_module_suite() From 07b258e2085d3397400ce305c2074b54bb232da0 Mon Sep 17 00:00:00 2001 From: kuantkid Date: Mon, 10 Sep 2012 13:11:27 +0800 Subject: [PATCH 537/648] FIX: multiply by the maximum value may change dtype --- skimage/util/dtype.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index b44bebbc..cc492035 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -152,12 +152,13 @@ def convert(image, dtype, force_copy=False, uniform=False): if kind == 'b': # to binary image + sign_loss() prec_loss() return dtype(image) if kind_in == 'b': # from binary image, to float and to integer - return dtype(image) * dtype_range[dtype][1] + return dtype(image) * dtype(dtype_range[dtype][1]) if kind in 'ui': imin = np.iinfo(dtype).min From 554487f9de4b96671daf1cc4bb281aa844db4ac6 Mon Sep 17 00:00:00 2001 From: kuantkid Date: Mon, 10 Sep 2012 13:22:38 +0800 Subject: [PATCH 538/648] FIX: issue sign loss warning only for signed type --- skimage/util/dtype.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index cc492035..518ebf18 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -152,7 +152,8 @@ def convert(image, dtype, force_copy=False, uniform=False): if kind == 'b': # to binary image - sign_loss() + if kind_in in "fi": + sign_loss() prec_loss() return dtype(image) From dc90c7a84fab3e7c211516e2ebd81e6759e8da6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 10 Sep 2012 09:06:06 +0200 Subject: [PATCH 539/648] Make font-size of documentation smaller --- doc/source/themes/agogo/static/agogo.css_t | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/themes/agogo/static/agogo.css_t b/doc/source/themes/agogo/static/agogo.css_t index 2ff4abad..f47167c0 100644 --- a/doc/source/themes/agogo/static/agogo.css_t +++ b/doc/source/themes/agogo/static/agogo.css_t @@ -23,6 +23,7 @@ div.header-wrapper { body { font-family: {{ theme_bodyfont }}; + font-size: 10pt; line-height: 1.4em; color: black; background-color: {{ theme_bgcolor }}; From cd4d50de7242dfdcaaca8817e346585323819066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 11 Sep 2012 09:04:49 +0200 Subject: [PATCH 540/648] Remove scipy build dependency --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 396d8429..b2f74c13 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,6 @@ VERSION = '0.7dev' PYTHON_VERSION = (2, 5) DEPENDENCIES = { 'numpy': (1, 6), - 'scipy': (0, 10), 'Cython': (0, 15), } From b1d99be7352bbaa4e036858e32c27943a6998dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 11 Sep 2012 07:41:53 +0200 Subject: [PATCH 541/648] Fix bug in grey erosion and dilation --- skimage/morphology/cmorph.pyx | 6 +++--- skimage/morphology/tests/test_grey.py | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index fb1e3af1..9b8b3a27 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -51,7 +51,7 @@ def dilate(np.ndarray[np.uint8_t, ndim=2] image, rr = r + sr[s] cc = c + sc[s] if 0 <= rr < rows and 0 <= cc < cols: - value = image_data[rr * rows + cc] + value = image_data[rr * cols + cc] if value > local_max: local_max = value @@ -85,7 +85,7 @@ def erode(np.ndarray[np.uint8_t, ndim=2] image, cdef np.uint8_t* out_data = out.data cdef np.uint8_t* image_data = image.data - cdef int r, c, rr, cc, s, value, local_max + cdef int r, c, rr, cc, s, value, local_min cdef int selem_num = np.sum(selem != 0) cdef int* sr = malloc(selem_num * sizeof(int)) @@ -106,7 +106,7 @@ def erode(np.ndarray[np.uint8_t, ndim=2] image, rr = r + sr[s] cc = c + sc[s] if 0 <= rr < rows and 0 <= cc < cols: - value = image_data[rr * rows + cc] + value = image_data[rr * cols + cc] if value < local_min: local_min = value diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 6003425f..6d2b75e8 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -5,6 +5,7 @@ from numpy import testing import skimage from skimage import data_dir +from skimage.util import img_as_bool from skimage.morphology import binary, grey, selem @@ -157,28 +158,28 @@ class TestDTypes(): def test_binary_erosion(): strel = selem.square(3) binary_res = binary.binary_erosion(bw_lena, strel) - grey_res = grey.erosion(bw_lena, strel) + grey_res = img_as_bool(grey.erosion(bw_lena, strel)) testing.assert_array_equal(binary_res, grey_res) def test_binary_dilation(): strel = selem.square(3) binary_res = binary.binary_dilation(bw_lena, strel) - grey_res = grey.dilation(bw_lena, strel) + grey_res = img_as_bool(grey.dilation(bw_lena, strel)) testing.assert_array_equal(binary_res, grey_res) def test_binary_closing(): strel = selem.square(3) binary_res = binary.binary_closing(bw_lena, strel) - grey_res = grey.closing(bw_lena, strel) + grey_res = img_as_bool(grey.closing(bw_lena, strel)) testing.assert_array_equal(binary_res, grey_res) def test_binary_opening(): strel = selem.square(3) binary_res = binary.binary_opening(bw_lena, strel) - grey_res = grey.opening(bw_lena, strel) + grey_res = img_as_bool(grey.opening(bw_lena, strel)) testing.assert_array_equal(binary_res, grey_res) From a7f36f5fc1272715740ec2a46c6ded15feabb6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 11 Sep 2012 09:48:22 +0200 Subject: [PATCH 542/648] Let travis build bot run tests --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f2708daa..566f7ba7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,4 +23,4 @@ script: # Change into an innocuous directory and find tests from installation - mkdir for_test - cd for_test - - nosetests `$PYTHON -c "import os; import skimage; print(os.path.dirname(skimage.__file__))"` + - $PYTHON -c "import skimage; assert skimage.test();" From f478b6a397e2efc481844c52133c2e909243257c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 11 Sep 2012 20:11:55 +0200 Subject: [PATCH 543/648] Fix bug in binary dilation and test case --- skimage/morphology/binary.py | 2 +- skimage/morphology/tests/test_grey.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index ffd79482..e2e0f20b 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -65,7 +65,7 @@ def binary_dilation(image, selem, out=None): """ conv = ndimage.convolve(image > 0, selem, output=out, - mode='constant', cval=1) + mode='constant', cval=0) if conv is not None: out = conv return np.not_equal(out, 0, out=out) diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 6d2b75e8..31a33321 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -10,7 +10,7 @@ from skimage.morphology import binary, grey, selem lena = np.load(os.path.join(data_dir, 'lena_GRAY_U8.npy')) -bw_lena = lena > 0.4 +bw_lena = lena > 100 class TestMorphology(): From 56b147678943dbd5a1e480f253b74f7864aff4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 11 Sep 2012 20:27:18 +0200 Subject: [PATCH 544/648] Add test case for non-square images --- skimage/morphology/tests/test_grey.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 31a33321..244ec566 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -155,6 +155,13 @@ class TestDTypes(): self._test_image(image) +def test_non_square_image(): + strel = selem.square(3) + binary_res = binary.binary_erosion(bw_lena[:100, :200], strel) + grey_res = img_as_bool(grey.erosion(bw_lena[:100, :200], strel)) + testing.assert_array_equal(binary_res, grey_res) + + def test_binary_erosion(): strel = selem.square(3) binary_res = binary.binary_erosion(bw_lena, strel) From ef06d4e474a0ed7889b8a2f9eca47af913399f49 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Tue, 11 Sep 2012 11:56:34 -0700 Subject: [PATCH 545/648] TST: Invoke Travis nosetests differently. --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 566f7ba7..28104a17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ # We pretend to be erlang because we need can't use the python support in # travis-ci; it uses virtualenvs, they do not have numpy, scipy, matplotlib, # and it is impractical to build them + language: erlang env: - PYTHON=python PYSUF='' @@ -23,4 +24,5 @@ script: # Change into an innocuous directory and find tests from installation - mkdir for_test - cd for_test - - $PYTHON -c "import skimage; assert skimage.test();" + - nosetests --exe -v --cover-package=skimage skimage + From 2614853a923a9e10b9a6c7b74ab50b269319b8d4 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 11 Sep 2012 23:33:15 -0400 Subject: [PATCH 546/648] DOC: suppress plot display in doctests Plot display interrupts the doctest run. --- skimage/transform/finite_radon_transform.py | 2 +- skimage/transform/hough_transform.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/transform/finite_radon_transform.py b/skimage/transform/finite_radon_transform.py index 13007dce..fa84c5e3 100644 --- a/skimage/transform/finite_radon_transform.py +++ b/skimage/transform/finite_radon_transform.py @@ -52,7 +52,7 @@ def frt2(a): >>> plt.imshow(f, interpolation='nearest', cmap=plt.cm.gray) >>> plt.xlabel('Angle') >>> plt.ylabel('Translation') - >>> plt.show() + >>> # plt.show() References ---------- diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index 5b36b103..3c3a35de 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -137,7 +137,7 @@ def hough(img, theta=None): >>> plt.imshow(out, cmap=plt.cm.bone) >>> plt.xlabel('Angle (degree)') >>> plt.ylabel('Distance %d (pixel)' % d[0]) - >>> plt.show() + >>> # plt.show() .. plot:: hough_tf.py From 24f3b7d26008c6954b71bfc65c2d41a970e27c75 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 11 Sep 2012 23:35:09 -0400 Subject: [PATCH 547/648] DOC: Fix block-formatting in doctest --- skimage/transform/hough_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index 3c3a35de..0e1fb9bf 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -124,7 +124,7 @@ def hough(img, theta=None): >>> img[:, 65] = 1 >>> img[35:45, 35:50] = 1 >>> for i in range(90): - >>> img[i, i] = 1 + ... img[i, i] = 1 >>> img += np.random.random(img.shape) > 0.95 Apply the Hough transform: From 0504392fd5f730b0fcdcb37660cff89829b4870e Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 11 Sep 2012 23:36:12 -0400 Subject: [PATCH 548/648] DOC: Fix doctests in io collection module --- skimage/io/collection.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 37eacfff..d420b58d 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -56,9 +56,10 @@ def alphanumeric_key(s): -------- >>> alphanumeric_key('z23a') ['z', 23, 'a'] - >>> filenames = ['f9.10.png', 'f9.9.png', 'f10.10.png', 'f10.9.png'] + >>> filenames = ['f9.10.png', 'e10.png', 'f9.9.png', 'f10.10.png', + ... 'f10.9.png'] >>> sorted(filenames) - ['f10.10.png', 'f10.9.png', 'f9.10.png', 'f9.9.png', 'e10.png'] + ['e10.png', 'f10.10.png', 'f10.9.png', 'f9.10.png', 'f9.9.png'] >>> sorted(filenames, key=alphanumeric_key) ['e10.png', 'f9.9.png', 'f9.10.png', 'f10.9.png', 'f10.10.png'] """ @@ -284,7 +285,7 @@ class ImageCollection(object): >>> len(coll) 2 >>> coll[0].shape - (128, 128, 3) + (512, 512, 3) >>> ic = io.ImageCollection('/tmp/work/*.png:/tmp/other/*.jpg') From 7134a2609f72f34177919de9023630d3bf34aaa7 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 11 Sep 2012 23:37:07 -0400 Subject: [PATCH 549/648] DOC: Fix array formatting in doctest --- .../random_walker_segmentation.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 52bf4425..7597d5b6 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -324,16 +324,16 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, >>> b[3,3] = 1 #Marker for first phase >>> b[6,6] = 2 #Marker for second phase >>> random_walker(a, b) - array([[ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 1., 1., 1., 1., 2., 2., 2., 1., 1.], - [ 1., 1., 1., 1., 1., 2., 2., 2., 1., 1.], - [ 1., 1., 1., 1., 1., 2., 2., 2., 1., 1.], - [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]]) + 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], + [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], + [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], + [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], dtype=int32) """ # Parse input data From fcf2a9bc970a96d698c8ada44c98de81520dc411 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 11 Sep 2012 23:38:00 -0400 Subject: [PATCH 550/648] DOC: Fix doctests in viewer subpackage The doctests in the viewer subpackage weren't originally written as proper doctests. --- skimage/viewer/plugins/base.py | 26 +++++++++++++------------- skimage/viewer/viewers/core.py | 5 +++-- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 074e18d4..198bac6a 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -54,17 +54,17 @@ class Plugin(QDialog): Examples -------- - >>> def my_func(image, arg1, arg2, optional_arg=0): - >>> ... + >>> from skimage.viewer import ImageViewer + >>> from skimage.viewer.widgets import Slider + >>> from skimage import data >>> + >>> plugin = Plugin(image_filter=lambda img, threshold: img > threshold) + >>> plugin += Slider('threshold', 0, 255) + >>> + >>> image = data.coins() >>> viewer = ImageViewer(image) - >>> - >>> plugin = Plugin(image_filter=my_func) - >>> plugin += Widget('arg1', ..., ptype='arg') - >>> plugin += Widget('arg2', ..., ptype='arg') - >>> plugin += Widget('optional_arg', ..., ptype='kwarg') - >>> - >>> viewer.show() + >>> viewer += plugin + >>> # viewer.show() The plugin will automatically delegate parameters to `image_filter` based on its parameter type, i.e., `ptype` (widgets for required arguments must @@ -108,9 +108,9 @@ class Plugin(QDialog): """Attach the plugin to an ImageViewer. Note that the ImageViewer will automatically call this method when the - plugin is added to the ImageViewer. For example: + plugin is added to the ImageViewer. For example:: - >>> viewer += Plugin(...) + viewer += Plugin(...) Also note that `attach` automatically calls the filter function so that the image matches the filtered value specified by attached widgets. @@ -131,9 +131,9 @@ class Plugin(QDialog): def add_widget(self, widget): """Add widget to plugin. - Alternatively, Plugin's `__add__` method is overloaded to add widgets: + Alternatively, Plugin's `__add__` method is overloaded to add widgets:: - >>> plugin += Widget(...) + plugin += Widget(...) Widgets can adjust required or optional arguments of filter function or parameters for the plugin. This is specified by the Widget's `ptype'. diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 490ff92c..7aadb571 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -49,9 +49,10 @@ class ImageViewer(QMainWindow): Examples -------- + >>> from skimage import data + >>> image = data.coins() >>> viewer = ImageViewer(image) - >>> viewer += SomePlugin() - >>> viewer.show() + >>> # viewer.show() """ def __init__(self, image): From a58ce5b5719e89db676ae6070a45dff961871195 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 11 Sep 2012 23:39:08 -0400 Subject: [PATCH 551/648] DOC: Add missing import in doctest --- skimage/graph/mcp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skimage/graph/mcp.py b/skimage/graph/mcp.py index 28843cbf..27b7062f 100644 --- a/skimage/graph/mcp.py +++ b/skimage/graph/mcp.py @@ -39,7 +39,9 @@ def route_through_array(array, start, end, fully_connected=True, Examples -------- + >>> import numpy as np >>> from skimage.graph import route_through_array + >>> >>> image = np.array([[1, 3], [10, 12]]) >>> image array([[ 1, 3], From 7a9cb807d11754661c98baa3c4c9ef876b0dff99 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 11 Sep 2012 23:42:22 -0400 Subject: [PATCH 552/648] DOC: Fix doctests in lpi_filter --- skimage/filter/lpi_filter.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index 60eb1d63..d023185f 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -58,8 +58,12 @@ class LPIFilter2D(object): In other words, example would be called like this: + >>> def impulse_response(r, c, **filter_params): + ... pass + >>> >>> r = [0,0,0,1,1,1,2,2,2] >>> c = [0,1,2,0,1,2,0,1,2] + >>> filter_params = {'kw1': 1, 'kw2': 2, 'kw3': 3} >>> impulse_response(r, c, **filter_params) Examples @@ -68,8 +72,7 @@ class LPIFilter2D(object): Gaussian filter: >>> def filt_func(r, c): - return np.exp(-np.hypot(r, c)/1) - + ... return np.exp(-np.hypot(r, c)/1) >>> filter = LPIFilter2D(filt_func) """ @@ -149,9 +152,10 @@ def forward(data, impulse_response=None, filter_params={}, Gaussian filter: >>> def filt_func(r, c): - return np.exp(-np.hypot(r, c)/1) - - >>> forward(data, filt_func) + ... return np.exp(-np.hypot(r, c)/1) + >>> + >>> from skimage import data + >>> filtered = forward(data.coins(), filt_func) """ if predefined_filter is None: From ae429011cf477a29e5818160075f05fa77e7ec17 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 11 Sep 2012 23:46:26 -0400 Subject: [PATCH 553/648] DOC: rewrap docstring lines and remove unused import --- skimage/filter/lpi_filter.py | 40 ++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index d023185f..3826f5e7 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -7,7 +7,7 @@ __all__ = ['inverse', 'wiener', 'LPIFilter2D'] __docformat__ = 'restructuredtext en' import numpy as np -from scipy.fftpack import fftshift, ifftshift +from scipy.fftpack import ifftshift eps = np.finfo(float).eps @@ -50,13 +50,12 @@ class LPIFilter2D(object): Parameters ---------- impulse_response : callable `f(r, c, **filter_params)` - Function that yields the impulse response. `r` and - `c` are 1-dimensional vectors that represent row and - column positions, in other words coordinates are - (r[0],c[0]),(r[0],c[1]) etc. `**filter_params` are - passed through. + Function that yields the impulse response. `r` and `c` are + 1-dimensional vectors that represent row and column positions, in + other words coordinates are (r[0],c[0]),(r[0],c[1]) etc. + `**filter_params` are passed through. - In other words, example would be called like this: + In other words, `impulse_response` would be called like this: >>> def impulse_response(r, c, **filter_params): ... pass @@ -116,8 +115,9 @@ class LPIFilter2D(object): def __call__(self, data): """Apply the filter to the given data. - *Parameters*: - data : (M,N) ndarray + Parameters + ---------- + data : (M,N) ndarray """ F, G = self._prepare(data) @@ -142,9 +142,8 @@ def forward(data, impulse_response=None, filter_params={}, Other Parameters ---------------- predefined_filter : LPIFilter2D - If you need to apply the same filter multiple times over - different images, construct the LPIFilter2D and specify - it here. + If you need to apply the same filter multiple times over different + images, construct the LPIFilter2D and specify it here. Examples -------- @@ -176,17 +175,15 @@ def inverse(data, impulse_response=None, filter_params={}, max_gain=2, filter_params : dict Additional keyword parameters to the impulse_response function. max_gain : float - Limit the filter gain. Often, the filter contains - zeros, which would cause the inverse filter to have - infinite gain. High gain causes amplification of - artefacts, so a conservative limit is recommended. + Limit the filter gain. Often, the filter contains zeros, which would + cause the inverse filter to have infinite gain. High gain causes + amplification of artefacts, so a conservative limit is recommended. Other Parameters ---------------- predefined_filter : LPIFilter2D - If you need to apply the same filter multiple times over - different images, construct the LPIFilter2D and specify - it here. + If you need to apply the same filter multiple times over different + images, construct the LPIFilter2D and specify it here. """ if predefined_filter is None: @@ -223,9 +220,8 @@ def wiener(data, impulse_response=None, filter_params={}, K=0.25, Other Parameters ---------------- predefined_filter : LPIFilter2D - If you need to apply the same filter multiple times over - different images, construct the LPIFilter2D and specify - it here. + If you need to apply the same filter multiple times over different + images, construct the LPIFilter2D and specify it here. """ if predefined_filter is None: From 538ce329a5db2c2a425422b275a7c5590e94b863 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 11 Sep 2012 23:47:18 -0400 Subject: [PATCH 554/648] DOC: fix numpy dtype print out. This fix may be very dependent on numpy version since print out has change over time. --- skimage/morphology/grey.py | 12 ++++++------ skimage/morphology/watershed.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index dc34d3d6..a7959bb6 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -57,7 +57,7 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): [0, 0, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]], dtype='uint8') + [0, 0, 0, 0, 0]], dtype=uint8) """ @@ -109,7 +109,7 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): [0, 1, 1, 1, 0], [0, 1, 1, 1, 0], [0, 1, 1, 1, 0], - [0, 0, 0, 0, 0]], dtype='uint8') + [0, 0, 0, 0, 0]], dtype=uint8) """ @@ -158,7 +158,7 @@ def opening(image, selem, out=None): [1, 1, 0, 1, 1], [1, 1, 0, 1, 1], [1, 1, 0, 1, 1], - [0, 0, 0, 0, 0]], dtype='uint8') + [0, 0, 0, 0, 0]], dtype=uint8) """ @@ -208,7 +208,7 @@ def closing(image, selem, out=None): [0, 0, 0, 0, 0], [1, 1, 1, 1, 1], [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]], dtype='uint8') + [0, 0, 0, 0, 0]], dtype=uint8) """ @@ -257,7 +257,7 @@ def white_tophat(image, selem, out=None): [0, 0, 1, 0, 0], [0, 1, 5, 1, 0], [0, 0, 1, 0, 0], - [0, 0, 0, 0, 0]], dtype='uint8') + [0, 0, 0, 0, 0]], dtype=uint8) """ if image is out: @@ -306,7 +306,7 @@ def black_tophat(image, selem, out=None): [0, 0, 1, 0, 0], [0, 1, 5, 1, 0], [0, 0, 1, 0, 0], - [0, 0, 0, 0, 0]], dtype='uint8') + [0, 0, 0, 0, 0]], dtype=uint8) """ diff --git a/skimage/morphology/watershed.py b/skimage/morphology/watershed.py index c0b1b34b..8a08fafc 100644 --- a/skimage/morphology/watershed.py +++ b/skimage/morphology/watershed.py @@ -261,7 +261,7 @@ def is_local_maximum(image, labels=None, footprint=None): array([[ True, False, False, False], [ True, False, True, False], [ True, False, False, False], - [ True, True, False, True]], dtype='bool') + [ True, True, False, True]], dtype=bool) >>> image = np.arange(16).reshape((4, 4)) >>> labels = np.array([[1, 2], [3, 4]]) >>> labels = np.repeat(np.repeat(labels, 2, axis=0), 2, axis=1) @@ -279,7 +279,7 @@ def is_local_maximum(image, labels=None, footprint=None): array([[False, False, False, False], [False, True, False, True], [False, False, False, False], - [False, True, False, True]], dtype='bool') + [False, True, False, True]], dtype=bool) """ if labels is None: labels = np.ones(image.shape, dtype=np.uint8) From 1108727ed86454894523081fa6c39400d5b3127b Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 12 Sep 2012 21:38:00 -0400 Subject: [PATCH 555/648] DOC: Replace doctest with literal code block --- skimage/transform/finite_radon_transform.py | 12 ++++++------ skimage/transform/hough_transform.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/skimage/transform/finite_radon_transform.py b/skimage/transform/finite_radon_transform.py index fa84c5e3..f2d4705d 100644 --- a/skimage/transform/finite_radon_transform.py +++ b/skimage/transform/finite_radon_transform.py @@ -46,13 +46,13 @@ def frt2(a): >>> f = frt2(img) - Plot the results: + Plot the results:: - >>> import matplotlib.pyplot as plt - >>> plt.imshow(f, interpolation='nearest', cmap=plt.cm.gray) - >>> plt.xlabel('Angle') - >>> plt.ylabel('Translation') - >>> # plt.show() + import matplotlib.pyplot as plt + plt.imshow(f, interpolation='nearest', cmap=plt.cm.gray) + plt.xlabel('Angle') + plt.ylabel('Translation') + plt.show() References ---------- diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index 0e1fb9bf..563f9d1b 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -131,13 +131,13 @@ def hough(img, theta=None): >>> out, angles, d = hough(img) - Plot the results: + Plot the results:: - >>> import matplotlib.pyplot as plt - >>> plt.imshow(out, cmap=plt.cm.bone) - >>> plt.xlabel('Angle (degree)') - >>> plt.ylabel('Distance %d (pixel)' % d[0]) - >>> # plt.show() + import matplotlib.pyplot as plt + plt.imshow(out, cmap=plt.cm.bone) + plt.xlabel('Angle (degree)') + plt.ylabel('Distance %d (pixel)' % d[0]) + plt.show() .. plot:: hough_tf.py From b433e8ecef09c2d5501f946af974aea2e290bc97 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 13 Sep 2012 09:28:12 -0400 Subject: [PATCH 556/648] DOC: Remove plots from docstring examples Plotting isn't really necessary in a docstring example. Both of these functions already have gallery examples. --- skimage/transform/finite_radon_transform.py | 8 -------- skimage/transform/hough_transform.py | 8 -------- 2 files changed, 16 deletions(-) diff --git a/skimage/transform/finite_radon_transform.py b/skimage/transform/finite_radon_transform.py index f2d4705d..c107546f 100644 --- a/skimage/transform/finite_radon_transform.py +++ b/skimage/transform/finite_radon_transform.py @@ -46,14 +46,6 @@ def frt2(a): >>> f = frt2(img) - Plot the results:: - - import matplotlib.pyplot as plt - plt.imshow(f, interpolation='nearest', cmap=plt.cm.gray) - plt.xlabel('Angle') - plt.ylabel('Translation') - plt.show() - References ---------- .. [FRT] A. Kingston and I. Svalbe, "Projective transforms on periodic diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index 563f9d1b..4e3acd6e 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -131,14 +131,6 @@ def hough(img, theta=None): >>> out, angles, d = hough(img) - Plot the results:: - - import matplotlib.pyplot as plt - plt.imshow(out, cmap=plt.cm.bone) - plt.xlabel('Angle (degree)') - plt.ylabel('Distance %d (pixel)' % d[0]) - plt.show() - .. plot:: hough_tf.py """ From 56b4ba67e617117b88d8a9b75535e5957995bbc4 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 10 Sep 2012 22:47:10 -0700 Subject: [PATCH 557/648] ENH: Bool dtype conversion speed improvements. --- skimage/util/dtype.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index 518ebf18..2b971b9c 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -155,11 +155,14 @@ def convert(image, dtype, force_copy=False, uniform=False): if kind_in in "fi": sign_loss() prec_loss() - return dtype(image) - + return image > dtype_in(0) + if kind_in == 'b': # from binary image, to float and to integer - return dtype(image) * dtype(dtype_range[dtype][1]) + result = dtype(image) + if kind != 'f': + result *= dtype(dtype_range[dtype][1]) + return result if kind in 'ui': imin = np.iinfo(dtype).min From 8494a1888266bd23b03aead8db7521f9f4d20263 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 10 Sep 2012 23:13:14 -0700 Subject: [PATCH 558/648] Conversion to bool: threshold on half dtype max. --- skimage/util/dtype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index 2b971b9c..eef7f478 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -155,7 +155,7 @@ def convert(image, dtype, force_copy=False, uniform=False): if kind_in in "fi": sign_loss() prec_loss() - return image > dtype_in(0) + return image > dtype_in(dtype_range[dtype_in][1] / 2) if kind_in == 'b': # from binary image, to float and to integer From 81e49b6686fc7263df5d51f28827baac136db022 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 13 Sep 2012 21:37:52 -0400 Subject: [PATCH 559/648] DOC: Fix docstring note about conversion range --- skimage/util/dtype.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index eef7f478..d2a0dd61 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -360,7 +360,8 @@ def img_as_bool(image, force_copy=False): Notes ----- - All non-zero elements are treated as True. + The upper half of the input dtype's positive range is True, and the lower + half is False. All negative values (if present) are False. """ return convert(image, np.bool_, force_copy) From f2d5b109e90dde5ba50a5717428d91a85d85003f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 13 Sep 2012 22:15:14 -0400 Subject: [PATCH 560/648] Add `assert_greater` compatibility function. Fix tests to work with nose < 1.1.3. Compatibility functions borrowed from scikit-learn. --- skimage/_shared/testing.py | 28 +++++++++++++++++++ .../segmentation/tests/test_felzenszwalb.py | 2 +- skimage/segmentation/tests/test_quickshift.py | 3 +- 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 skimage/_shared/testing.py diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py new file mode 100644 index 00000000..a5270125 --- /dev/null +++ b/skimage/_shared/testing.py @@ -0,0 +1,28 @@ +"""Testing utilities.""" + +# Copyright (c) 2011 Pietro Berkes +# License: Simplified BSD + +def _assert_less(a, b, msg=None): + message = "%r is not lower than %r" % (a, b) + if msg is not None: + message += ": " + msg + assert a < b, message + + +def _assert_greater(a, b, msg=None): + message = "%r is not greater than %r" % (a, b) + if msg is not None: + message += ": " + msg + assert a > b, message + + +try: + from nose.tools import assert_less +except ImportError: + assert_less = _assert_less + +try: + from nose.tools import assert_greater +except ImportError: + assert_greater = _assert_greater diff --git a/skimage/segmentation/tests/test_felzenszwalb.py b/skimage/segmentation/tests/test_felzenszwalb.py index 8a7abfc4..9fd0b018 100644 --- a/skimage/segmentation/tests/test_felzenszwalb.py +++ b/skimage/segmentation/tests/test_felzenszwalb.py @@ -1,6 +1,6 @@ import numpy as np from numpy.testing import assert_equal, assert_array_equal -from nose.tools import assert_greater +from skimage._shared.testing import assert_greater from skimage.segmentation import felzenszwalb diff --git a/skimage/segmentation/tests/test_quickshift.py b/skimage/segmentation/tests/test_quickshift.py index eebcaf0d..d43d7559 100644 --- a/skimage/segmentation/tests/test_quickshift.py +++ b/skimage/segmentation/tests/test_quickshift.py @@ -1,6 +1,7 @@ import numpy as np from numpy.testing import assert_equal, assert_array_equal -from nose.tools import assert_true, assert_greater +from nose.tools import assert_true +from skimage._shared.testing import assert_greater from skimage.segmentation import quickshift From 6d884906572b277202f439f161a94e6b3f4f1ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 15 Sep 2012 10:40:52 +0200 Subject: [PATCH 561/648] Rename pyramid functions --- doc/examples/plot_pyramid.py | 4 ++-- skimage/transform/__init__.py | 2 +- skimage/transform/pyramids.py | 8 ++++---- skimage/transform/tests/test_pyramids.py | 6 +++--- viewer_examples/viewers/collection_viewer.py | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/examples/plot_pyramid.py b/doc/examples/plot_pyramid.py index 60067549..47ac1e16 100644 --- a/doc/examples/plot_pyramid.py +++ b/doc/examples/plot_pyramid.py @@ -13,12 +13,12 @@ import numpy as np import matplotlib.pyplot as plt from skimage import data -from skimage.transform import build_gaussian_pyramid +from skimage.transform import pyramid_gaussian image = data.lena() rows, cols, dim = image.shape -pyramid = tuple(build_gaussian_pyramid(image, downscale=2)) +pyramid = tuple(pyramid_gaussian(image, downscale=2)) composite_image = np.zeros((rows, cols + cols / 2, 3), dtype=np.double) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index fc2b12ef..a55d5df4 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -8,4 +8,4 @@ from ._geometric import (warp, warp_coords, estimate_transform, PiecewiseAffineTransform) from ._warps import swirl, homography, resize, rotate from .pyramids import (pyramid_reduce, pyramid_expand, - build_gaussian_pyramid, build_laplacian_pyramid) + pyramid_gaussian, pyramid_laplacian) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index f07fb1c5..a7492043 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -135,8 +135,8 @@ def pyramid_expand(image, upscale=2, sigma=None, order=1, return out -def build_gaussian_pyramid(image, max_layer=-1, downscale=2, sigma=None, - order=1, mode='reflect', cval=0): +def pyramid_gaussian(image, max_layer=-1, downscale=2, sigma=None, order=1, + mode='reflect', cval=0): """Yield images of the gaussian pyramid formed by the input image. Recursively applies the `pyramid_reduce` function to the image, and yields @@ -209,8 +209,8 @@ def build_gaussian_pyramid(image, max_layer=-1, downscale=2, sigma=None, yield layer_image -def build_laplacian_pyramid(image, max_layer=-1, downscale=2, sigma=None, - order=1, mode='reflect', cval=0): +def pyramid_laplacian(image, max_layer=-1, downscale=2, sigma=None, order=1, + mode='reflect', cval=0): """Yield images of the laplacian pyramid formed by the input image. Each layer contains the difference between the downsampled and the diff --git a/skimage/transform/tests/test_pyramids.py b/skimage/transform/tests/test_pyramids.py index 9aa236ff..611ecdec 100644 --- a/skimage/transform/tests/test_pyramids.py +++ b/skimage/transform/tests/test_pyramids.py @@ -1,7 +1,7 @@ from numpy.testing import assert_array_equal, run_module_suite from skimage import data from skimage.transform import (pyramid_reduce, pyramid_expand, - build_gaussian_pyramid, build_laplacian_pyramid) + pyramid_gaussian, pyramid_laplacian) image = data.lena() @@ -21,7 +21,7 @@ def test_pyramid_expand(): def test_build_gaussian_pyramid(): rows, cols, dim = image.shape - pyramid = build_gaussian_pyramid(image, downscale=2) + pyramid = pyramid_gaussian(image, downscale=2) for layer, out in enumerate(pyramid): layer_shape = (rows / 2 ** layer, cols / 2 ** layer, dim) @@ -30,7 +30,7 @@ def test_build_gaussian_pyramid(): def test_build_laplacian_pyramid(): rows, cols, dim = image.shape - pyramid = build_laplacian_pyramid(image, downscale=2) + pyramid = pyramid_laplacian(image, downscale=2) for layer, out in enumerate(pyramid): layer_shape = (rows / 2 ** layer, cols / 2 ** layer, dim) diff --git a/viewer_examples/viewers/collection_viewer.py b/viewer_examples/viewers/collection_viewer.py index 24c97ab0..62cdbb26 100644 --- a/viewer_examples/viewers/collection_viewer.py +++ b/viewer_examples/viewers/collection_viewer.py @@ -21,11 +21,11 @@ home/end keys import numpy as np from skimage import data from skimage.viewer import CollectionViewer -from skimage.transform import build_gaussian_pyramid +from skimage.transform import pyramid_gaussian img = data.lena() -img_collection = tuple(build_gaussian_pyramid(img)) +img_collection = tuple(pyramid_gaussian(img)) view = CollectionViewer(img_collection) view.show() From bfcc144699573c131e5554a75e0e497299ed71ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 15 Sep 2012 10:41:08 +0200 Subject: [PATCH 562/648] Fix typo --- doc/examples/plot_pyramid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_pyramid.py b/doc/examples/plot_pyramid.py index 47ac1e16..8752af68 100644 --- a/doc/examples/plot_pyramid.py +++ b/doc/examples/plot_pyramid.py @@ -3,7 +3,7 @@ Build image pyramids ==================== -The `build_gauassian_pyramid` function takes an image and yields successive +The `build_gaussian_pyramid` function takes an image and yields successive images shrunk by a constant scale factor. Image pyramids are often used, e.g., to implement algorithms for denoising, texture discrimination, and scale- invariant detection. From be7476b44f0b840cc5c47cb4ba3861fa66d04940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 15 Sep 2012 10:58:28 +0200 Subject: [PATCH 563/648] Add more detailed description for pyramid functions --- skimage/transform/pyramids.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index a7492043..dd274834 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -140,8 +140,12 @@ def pyramid_gaussian(image, max_layer=-1, downscale=2, sigma=None, order=1, """Yield images of the gaussian pyramid formed by the input image. Recursively applies the `pyramid_reduce` function to the image, and yields - the downscaled images. Note that the first image of the pyramid will be the - original, unscaled image. + the downscaled images. + + Note that the first image of the pyramid will be the original, unscaled + image. The total number of images is `max_layer + 1`. In case all layers are + computed, the last image is either a one-pixel image or the image where the + reduction does not change its shape. Parameters ---------- @@ -214,7 +218,15 @@ def pyramid_laplacian(image, max_layer=-1, downscale=2, sigma=None, order=1, """Yield images of the laplacian pyramid formed by the input image. Each layer contains the difference between the downsampled and the - downsampled plus smoothed image. + downsampled, smoothed image:: + + layer = resize(prev_layer) - smooth(resize(prev_layer)) + + Note that the first image of the pyramid will be the difference between the + original, unscaled image and its smoothed version. The total number of + images is `max_layer + 1`. In case all layers are computed, the last image + is either a one-pixel image or the image where the reduction does not change + its shape. Parameters ---------- From bada6787aa111feac1df32952a8732400632f81d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 15 Sep 2012 17:44:24 +0200 Subject: [PATCH 564/648] Update name of pyramid function in pyramid example description --- doc/examples/plot_pyramid.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/examples/plot_pyramid.py b/doc/examples/plot_pyramid.py index 8752af68..eb3896f4 100644 --- a/doc/examples/plot_pyramid.py +++ b/doc/examples/plot_pyramid.py @@ -3,10 +3,11 @@ Build image pyramids ==================== -The `build_gaussian_pyramid` function takes an image and yields successive -images shrunk by a constant scale factor. Image pyramids are often used, e.g., -to implement algorithms for denoising, texture discrimination, and scale- -invariant detection. +The `pyramid_gaussian` function takes an image and yields successive images +shrunk by a constant scale factor. Image pyramids are often used, e.g., to +implement algorithms for denoising, texture discrimination, and scale- invariant +detection. + """ import numpy as np From 2cbe2e1f20a6ac2d7de247a7ec960bb99d34a182 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 16 Sep 2012 15:44:52 -0400 Subject: [PATCH 565/648] DOC: Fix attribution for testing function The testing functions taken from scikit-learn were in a file with a copyright assigned to Pietro, but Andreas was resposible for the specific lines that were copied. --- CONTRIBUTORS.txt | 2 +- skimage/_shared/testing.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 4abe9256..5f375c39 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -73,7 +73,7 @@ From whom we borrowed the example generation tools. - Andreas Mueller - Example data set loader. + Example data set loader. Nosetest compatibility functions. Quickshift image segmentation, Felzenszwalbs fast graph based segmentation. - Yaroslav Halchenko diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py index a5270125..eab83a56 100644 --- a/skimage/_shared/testing.py +++ b/skimage/_shared/testing.py @@ -1,7 +1,5 @@ """Testing utilities.""" -# Copyright (c) 2011 Pietro Berkes -# License: Simplified BSD def _assert_less(a, b, msg=None): message = "%r is not lower than %r" % (a, b) From ee2092c6fa3c916bc3cbf36a4e40d428c300ff25 Mon Sep 17 00:00:00 2001 From: JDWarner Date: Mon, 17 Sep 2012 16:38:59 -0500 Subject: [PATCH 566/648] Documentation fix for `labels` input in random_walker. This fix was needed because `labels` claimed it should be "of same shape as `data`", but this is no longer always the case. When multichannel=True, `labels` should be shaped like a SINGLE channel of `data`, i.e. without the final dimension denoting channels. --- skimage/segmentation/random_walker_segmentation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 7597d5b6..22ce17e2 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -190,13 +190,15 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, channels. Data spacing is assumed isotropic unless depth keyword argument is used. - labels : array of ints, of same shape as `data` + labels : array of ints, of same shape as `data` without channels dimension Array of seed markers labeled with different positive integers for different phases. Zero-labeled pixels are unlabeled pixels. Negative labels correspond to inactive pixels that are not taken into account (they are removed from the graph). If labels are not consecutive integers, the labels array will be transformed so that - labels are consecutive. + labels are consecutive. In the multichannel case, `labels` should have + the same shape as a single channel of `data`, i.e. without the final + dimension denoting channels. beta : float Penalization coefficient for the random walker motion From 6635cf16db1eede5c7dd6e993da91d893c882964 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Tue, 18 Sep 2012 19:56:52 +0200 Subject: [PATCH 567/648] [BUG] Corrected a bug in the random walker that appeared * when returning the full probability instead of the segmentation * for three or more labels (two was OK) --- skimage/segmentation/random_walker_segmentation.py | 3 +-- skimage/segmentation/tests/test_random_walker.py | 9 +++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 52bf4425..01113f6f 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -401,9 +401,8 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, copy=True).reshape(dims) for Xline in X]) for i in range(1, int(labels.max()) + 1): mask_i = np.squeeze(labels == i) + X[:, mask_i] = 0 X[i - 1, mask_i] = 1 - X[np.setdiff1d(np.arange(0, labels.max(), dtype=np.int), - [i - 1]), mask_i] = 0 else: X = _clean_labels_ar(X + 1, labels).reshape(dims) return X diff --git a/skimage/segmentation/tests/test_random_walker.py b/skimage/segmentation/tests/test_random_walker.py index ecf59e99..7df0241c 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -58,8 +58,13 @@ def test_2d_bf(): return_full_prob=True) assert (full_prob_bf[1, 25:45, 40:60] >= full_prob_bf[0, 25:45, 40:60]).all() - return data, labels_bf, full_prob_bf - + # Now test with more than two labels + labels[55, 80] = 3 + full_prob_bf = random_walker(data, labels, beta=90, mode='bf', + return_full_prob=True) + assert (full_prob_bf[1, 25:45, 40:60] >= + full_prob_bf[0, 25:45, 40:60]).all() + assert len(full_prob_bf) == 3 def test_2d_cg(): lx = 70 From 0dbb77a359b8281a1afadba9a9c8ae2080ee6e95 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Thu, 20 Sep 2012 17:52:17 -0700 Subject: [PATCH 568/648] BUG: Fix homography. --- skimage/transform/_warps.py | 3 ++- skimage/transform/tests/test_warps.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 1ed0bed9..72f90050 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -1,5 +1,6 @@ import numpy as np -from ._geometric import warp, SimilarityTransform, AffineTransform +from ._geometric import (warp, SimilarityTransform, AffineTransform, + ProjectiveTransform) def resize(image, output_shape, order=1, mode='constant', cval=0.): diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index ac9272c6..e8dc3ee7 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -5,7 +5,7 @@ from scipy.ndimage import map_coordinates from skimage.transform import (warp, warp_coords, rotate, resize, AffineTransform, ProjectiveTransform, - SimilarityTransform) + SimilarityTransform, homography) from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray @@ -34,11 +34,15 @@ def test_homography(): [0, 0, 1]]) x90 = warp(x, - inverse_map=ProjectiveTransform(M).inverse, - order=1) + inverse_map=ProjectiveTransform(M).inverse, + order=1) assert_array_almost_equal(x90, np.rot90(x)) +def test_homography_basic(): + homography(np.random.random((25, 25)), np.eye(3)) + + def test_fast_homography(): img = rgb2gray(data.lena()).astype(np.uint8) img = img[:, :100] From c54d8d31f3a95a7734b30b2d5d9e58c1261b1a4b Mon Sep 17 00:00:00 2001 From: JDWarner Date: Mon, 24 Sep 2012 11:30:45 -0500 Subject: [PATCH 569/648] Docfix for img_as_float; appended myself to CONTRIBUTORS img_as_float() had a legacy note stating conversion would force negative values to the positive domain; this no longer described its functionality. Note changed to reflect the retention of negative values when converting from signed datatypes. When submitting my multichannel improvement to the random walker segmentation algorithm, I neglected to append my name to the CONTRIBUTORS file. Now fixed. --- CONTRIBUTORS.txt | 3 +++ skimage/util/dtype.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 5f375c39..0e3f1250 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -110,3 +110,6 @@ - Pavel Campr Fixes and tests for Histograms of Oriented Gradients. + +- Joshua Warner + Multichannel random walker segmentation. diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index d2a0dd61..9f804406 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -265,8 +265,8 @@ def img_as_float(image, force_copy=False): Notes ----- - The range of a floating point image is [0, 1]. - Negative input values will be shifted to the positive domain. + The range of a floating point image is [0.0, 1.0] or [-1.0, 1.0] when + converting from unsigned or signed datatypes, respectively. """ return convert(image, np.float64, force_copy) From 359e7703cc15adac28c232e0881bfdac0580d74e Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Tue, 25 Sep 2012 15:50:18 -0700 Subject: [PATCH 570/648] PKG: Remember to check that random.js file is correctly generated. --- RELEASE.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/RELEASE.txt b/RELEASE.txt index e024d57e..4821d68d 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -6,9 +6,10 @@ How to make a new release of ``skimage`` - Update the docs: - Edit ``doc/source/themes/agogo/static/docversions.js`` and commit - Build a clean version of the docs. Run "make" in the root dir, then - ``rm build -rf; make html`` in the docs. + ``rm build -rf; make html`` in the docs. Make sure the random.js - Run ``make html`` again to copy the newly generated ``random.js`` into - place. + place. Double check ``random.js``, otherwise the skimage.org front + page gets broken! - Push upstream using "make gh-pages" - Add the version number as a tag in git:: From 6c59e047143c82119f4a2003360058dcd2c3d665 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Thu, 27 Sep 2012 20:01:45 +0100 Subject: [PATCH 571/648] MISC remove unused imports, some pep8 corrections. --- skimage/feature/template.py | 2 - skimage/feature/texture.py | 6 +-- skimage/filter/_tv_denoise.py | 5 +- skimage/filter/tests/test_tv_denoise.py | 9 ++-- skimage/graph/mcp.py | 2 +- skimage/graph/tests/test_heap.py | 1 - skimage/io/_plugins/fits_plugin.py | 1 - skimage/io/_plugins/gdal_plugin.py | 2 - skimage/io/_plugins/qt_plugin.py | 3 +- skimage/io/_plugins/skivi.py | 6 +-- skimage/io/tests/test_histograms.py | 1 - skimage/io/tests/test_plugin_util.py | 9 ++-- skimage/io/tests/test_sift.py | 4 +- skimage/measure/_polygon.py | 1 - skimage/measure/_regionprops.py | 8 +-- .../tests/test_structural_similarity.py | 4 +- skimage/morphology/convex_hull.py | 2 +- skimage/morphology/grey.py | 1 - skimage/morphology/greyreconstruct.py | 1 - skimage/morphology/tests/test_skeletonize.py | 41 +++++++------- .../segmentation/tests/test_clear_border.py | 2 +- skimage/transform/_geometric.py | 15 +++--- skimage/transform/tests/test_warps.py | 2 +- skimage/util/tests/test_dtype.py | 3 +- skimage/viewer/plugins/lineprofile.py | 53 +++++++++++-------- skimage/viewer/utils/core.py | 8 +-- skimage/viewer/viewers/core.py | 7 ++- skimage/viewer/widgets/core.py | 2 +- 28 files changed, 97 insertions(+), 104 deletions(-) diff --git a/skimage/feature/template.py b/skimage/feature/template.py index 19d22c9b..51ca90b4 100644 --- a/skimage/feature/template.py +++ b/skimage/feature/template.py @@ -3,8 +3,6 @@ import numpy as np from . import _template -from skimage.util.dtype import convert - def match_template(image, template, pad_input=False): """Match a template to an image using normalized correlation. diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index 22b5d0d8..7655b82a 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -2,9 +2,7 @@ Methods to characterize image textures. """ -import math import numpy as np -from scipy import ndimage from ._texture import _glcm_loop, _local_binary_pattern @@ -236,8 +234,8 @@ def local_binary_pattern(image, P, R, method='default'): image : (N, M) array Graylevel image. P : int - Number of circularly symmetric neighbour set points (quantization of the - angular space). + Number of circularly symmetric neighbour set points (quantization of + the angular space). R : float Radius of circle (spatial resolution of the operator). method : {'default', 'ror', 'uniform', 'var'} diff --git a/skimage/filter/_tv_denoise.py b/skimage/filter/_tv_denoise.py index 4f663ae2..3302319c 100644 --- a/skimage/filter/_tv_denoise.py +++ b/skimage/filter/_tv_denoise.py @@ -170,7 +170,8 @@ def _tv_denoise_2d(im, weight=50, eps=2.e-4, n_iter_max=200): E_previous = E i += 1 return out - + + def tv_denoise(im, weight=50, eps=2.e-4, n_iter_max=200): """ Perform total-variation denoising on 2-d and 3-d images @@ -248,4 +249,4 @@ def tv_denoise(im, weight=50, eps=2.e-4, n_iter_max=200): else: raise ValueError('only 2-d and 3-d images may be denoised with this ' 'function') - return out + return out diff --git a/skimage/filter/tests/test_tv_denoise.py b/skimage/filter/tests/test_tv_denoise.py index 635dfdcd..cc4fae7e 100644 --- a/skimage/filter/tests/test_tv_denoise.py +++ b/skimage/filter/tests/test_tv_denoise.py @@ -2,7 +2,6 @@ import numpy as np from numpy.testing import run_module_suite from skimage import filter, data, color -from skimage import img_as_uint, img_as_ubyte class TestTvDenoise(): @@ -35,13 +34,13 @@ class TestTvDenoise(): # lena image lena = color.rgb2gray(data.lena())[:256, :256] int_lena = np.multiply(lena, 255).astype(np.uint8) - assert np.max(int_lena) > 1 + assert np.max(int_lena) > 1 denoised_int_lena = filter.tv_denoise(int_lena, weight=60.0) # test if the value range of output float data is within [0.0:1.0] assert denoised_int_lena.dtype == np.float assert np.max(denoised_int_lena) <= 1.0 - assert np.min(denoised_int_lena) >= 0.0 - + assert np.min(denoised_int_lena) >= 0.0 + def test_tv_denoise_3d(self): """ Apply the TV denoising algorithm on a 3D image representing @@ -56,7 +55,7 @@ class TestTvDenoise(): mask[mask > 255] = 255 res = filter.tv_denoise(mask.astype(np.uint8), weight=100) assert res.dtype == np.float - assert res.std() * 255 < mask.std() + assert res.std() * 255 < mask.std() # test wrong number of dimensions a = np.random.random((8, 8, 8, 8)) diff --git a/skimage/graph/mcp.py b/skimage/graph/mcp.py index 27b7062f..dc584226 100644 --- a/skimage/graph/mcp.py +++ b/skimage/graph/mcp.py @@ -1,4 +1,4 @@ -from ._mcp import MCP, MCP_Geometric, make_offsets +from ._mcp import MCP, MCP_Geometric def route_through_array(array, start, end, fully_connected=True, diff --git a/skimage/graph/tests/test_heap.py b/skimage/graph/tests/test_heap.py index 2dc7fb76..8322fd4e 100644 --- a/skimage/graph/tests/test_heap.py +++ b/skimage/graph/tests/test_heap.py @@ -1,4 +1,3 @@ -import numpy as np from numpy.testing import * import time diff --git a/skimage/io/_plugins/fits_plugin.py b/skimage/io/_plugins/fits_plugin.py index 9eb8e028..52785814 100644 --- a/skimage/io/_plugins/fits_plugin.py +++ b/skimage/io/_plugins/fits_plugin.py @@ -1,6 +1,5 @@ __all__ = ['imread', 'imread_collection'] -import numpy as np import skimage.io as io try: diff --git a/skimage/io/_plugins/gdal_plugin.py b/skimage/io/_plugins/gdal_plugin.py index f4c2a1ae..6749420f 100644 --- a/skimage/io/_plugins/gdal_plugin.py +++ b/skimage/io/_plugins/gdal_plugin.py @@ -1,7 +1,5 @@ __all__ = ['imread'] -import numpy as np - try: import osgeo.gdal as gdal except ImportError: diff --git a/skimage/io/_plugins/qt_plugin.py b/skimage/io/_plugins/qt_plugin.py index bb4cf232..24cf472c 100644 --- a/skimage/io/_plugins/qt_plugin.py +++ b/skimage/io/_plugins/qt_plugin.py @@ -1,6 +1,5 @@ -from .util import prepare_for_display, window_manager, GuiLockError +from .util import prepare_for_display, window_manager import numpy as np -import sys # We try to aquire the gui lock first or else the gui import might # trample another GUI's PyOS_InputHook. diff --git a/skimage/io/_plugins/skivi.py b/skimage/io/_plugins/skivi.py index 241bdf94..fb652638 100644 --- a/skimage/io/_plugins/skivi.py +++ b/skimage/io/_plugins/skivi.py @@ -14,13 +14,9 @@ The skivi module is not meant to be used directly. Use skimage.io.imshow(img, fancy=True)''' from textwrap import dedent -import numpy as np -import sys from PyQt4 import QtCore, QtGui -from PyQt4.QtGui import (QApplication, QMainWindow, QImage, QPixmap, - QLabel, QWidget, QVBoxLayout, QSlider, - QPainter, QColor, QFrame, QLayoutItem) +from PyQt4.QtGui import QMainWindow, QImage, QPixmap, QLabel, QWidget, QFrame from .q_color_mixer import MixerPanel from .q_histogram import QuadHistogram diff --git a/skimage/io/tests/test_histograms.py b/skimage/io/tests/test_histograms.py index 4ec099e3..bf972b17 100644 --- a/skimage/io/tests/test_histograms.py +++ b/skimage/io/tests/test_histograms.py @@ -1,7 +1,6 @@ from numpy.testing import * import numpy as np -import skimage.io._plugins._colormixer as cm from skimage.io._plugins._histograms import histograms diff --git a/skimage/io/tests/test_plugin_util.py b/skimage/io/tests/test_plugin_util.py index 09b758d8..41f28339 100644 --- a/skimage/io/tests/test_plugin_util.py +++ b/skimage/io/tests/test_plugin_util.py @@ -19,18 +19,18 @@ class TestPrepareForDisplay: assert x[3, 2, 0] == 255 def test_colour(self): - x = prepare_for_display(np.random.random((10, 10, 3))) + prepare_for_display(np.random.random((10, 10, 3))) def test_alpha(self): - x = prepare_for_display(np.random.random((10, 10, 4))) + prepare_for_display(np.random.random((10, 10, 4))) @raises(ValueError) def test_wrong_dimensionality(self): - x = prepare_for_display(np.random.random((10, 10, 1, 1))) + prepare_for_display(np.random.random((10, 10, 1, 1))) @raises(ValueError) def test_wrong_depth(self): - x = prepare_for_display(np.random.random((10, 10, 5))) + prepare_for_display(np.random.random((10, 10, 5))) class TestWindowManager: @@ -48,7 +48,6 @@ class TestWindowManager: self.callback_called = True def test_callback(self): - cb = lambda x: x self.wm.register_callback(self.callback) self.wm.add_window('window') self.wm.remove_window('window') diff --git a/skimage/io/tests/test_sift.py b/skimage/io/tests/test_sift.py index c488d52f..cabc8984 100644 --- a/skimage/io/tests/test_sift.py +++ b/skimage/io/tests/test_sift.py @@ -1,7 +1,5 @@ -import numpy as np from nose.tools import * -from numpy.testing import assert_array_equal, assert_array_almost_equal, \ - assert_equal, run_module_suite +from numpy.testing import assert_equal, run_module_suite from tempfile import NamedTemporaryFile import os diff --git a/skimage/measure/_polygon.py b/skimage/measure/_polygon.py index 633a9418..add4b5fb 100644 --- a/skimage/measure/_polygon.py +++ b/skimage/measure/_polygon.py @@ -53,7 +53,6 @@ def approximate_polygon(coords, tolerance): segment_coords = coords[start + 1:end, :] segment_dists = dists[start + 1:end] - # check whether to take perpendicular or euclidean distance with # inner product of vectors diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 142550bf..d285d453 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -159,15 +159,15 @@ def regionprops(label_image, properties=['Area', 'Centroid'], `pi/2` in counter-clockwise direction. * Perimeter : float - Perimeter of object which approximates the contour as a line through - the centers of border pixels using a 4-connectivity. + Perimeter of object which approximates the contour as a line + through the centers of border pixels using a 4-connectivity. * Solidity : float Ratio of pixels in the region to pixels of the convex hull image. * WeightedCentralMoments : (3, 3) ndarray - Central moments (translation invariant) of intensity image up to 3rd - order. + Central moments (translation invariant) of intensity image up to + 3rd order. wmu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } diff --git a/skimage/measure/tests/test_structural_similarity.py b/skimage/measure/tests/test_structural_similarity.py index 87846e6f..3eb2a7e9 100644 --- a/skimage/measure/tests/test_structural_similarity.py +++ b/skimage/measure/tests/test_structural_similarity.py @@ -2,7 +2,7 @@ import numpy as np from numpy.testing import assert_equal from skimage.measure import structural_similarity as ssim -import scipy.optimize as opt + def test_ssim_patch_range(): N = 51 @@ -12,6 +12,7 @@ def test_ssim_patch_range(): assert(ssim(X, Y, win_size=N) < 0.1) assert_equal(ssim(X, X, win_size=N), 1) + def test_ssim_image(): N = 100 X = (np.random.random((N, N)) * 255).astype(np.uint8) @@ -38,6 +39,7 @@ def test_ssim_image(): ## assert(np.all(opt.check_grad(func, grad, Y) < 0.05)) + def test_ssim_dtype(): N = 30 X = np.random.random((N, N)) diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index 6c4797ef..08ff0e04 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -1,7 +1,7 @@ __all__ = ['convex_hull_image'] import numpy as np -from ._pnpoly import points_inside_poly, grid_points_inside_poly +from ._pnpoly import grid_points_inside_poly from ._convex_hull import possible_hull diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index a7959bb6..e7e52de4 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -6,7 +6,6 @@ __docformat__ = 'restructuredtext en' import warnings -import numpy as np from skimage import img_as_ubyte from . import cmorph diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 1bbf33ca..09d3c9e6 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -191,4 +191,3 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): rec_img = value_map[value_rank[:image_stride]] rec_img.shape = np.array(seed.shape) + 2 * padding return rec_img[inside_slices] - diff --git a/skimage/morphology/tests/test_skeletonize.py b/skimage/morphology/tests/test_skeletonize.py index 709e1ba4..9c8fe249 100644 --- a/skimage/morphology/tests/test_skeletonize.py +++ b/skimage/morphology/tests/test_skeletonize.py @@ -35,7 +35,7 @@ class TestSkeletonize(): def test_skeletonize_all_foreground(self): im = np.ones((3, 4)) - result = skeletonize(im) + skeletonize(im) def test_skeletonize_single_point(self): im = np.zeros((5, 5), np.uint8) @@ -110,6 +110,7 @@ class TestSkeletonize(): [0, 0, 0, 0, 0, 0]], dtype=np.uint8) assert np.all(result == expected) + class TestMedialAxis(): def test_00_00_zeros(self): '''Test skeletonize on an array of all zeros''' @@ -130,15 +131,16 @@ class TestMedialAxis(): # The result should be four diagonals from the # corners, meeting in a horizontal line # - expected = np.array([[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,1,0], - [0,0,1,0,0,0,0,0,0,0,0,0,1,0,0], - [0,0,0,1,0,0,0,0,0,0,0,1,0,0,0], - [0,0,0,0,1,1,1,1,1,1,1,0,0,0,0], - [0,0,0,1,0,0,0,0,0,0,0,1,0,0,0], - [0,0,1,0,0,0,0,0,0,0,0,0,1,0,0], - [0,1,0,0,0,0,0,0,0,0,0,0,0,1,0], - [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], bool) + expected = np.array([[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, 1, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], + bool) result = medial_axis(image) assert np.all(result == expected) result, distance = medial_axis(image, return_distance=True) @@ -149,15 +151,16 @@ class TestMedialAxis(): image = np.zeros((9, 15), bool) image[1:-1, 1:-1] = True image[4, 4:-4] = False - expected = np.array([[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,1,0], - [0,0,1,1,1,1,1,1,1,1,1,1,1,0,0], - [0,0,1,0,0,0,0,0,0,0,0,0,1,0,0], - [0,0,1,0,0,0,0,0,0,0,0,0,1,0,0], - [0,0,1,0,0,0,0,0,0,0,0,0,1,0,0], - [0,0,1,1,1,1,1,1,1,1,1,1,1,0,0], - [0,1,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]],bool) + expected = np.array([[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, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 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]], + bool) result = medial_axis(image) assert np.all(result == expected) diff --git a/skimage/segmentation/tests/test_clear_border.py b/skimage/segmentation/tests/test_clear_border.py index d87f3d25..5d6852cf 100644 --- a/skimage/segmentation/tests/test_clear_border.py +++ b/skimage/segmentation/tests/test_clear_border.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import assert_array_equal, assert_equal +from numpy.testing import assert_array_equal from skimage.segmentation import clear_border diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index c3f21db9..6e04a271 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -269,8 +269,8 @@ class AffineTransform(ProjectiveTransform): for param in (scale, rotation, shear, translation)) if params and matrix is not None: - raise ValueError("You cannot specify the transformation matrix and " - "the implicit parameters at the same time.") + raise ValueError("You cannot specify the transformation matrix and" + " the implicit parameters at the same time.") elif matrix is not None: if matrix.shape != (3, 3): raise ValueError("Invalid shape of transformation matrix.") @@ -287,9 +287,9 @@ class AffineTransform(ProjectiveTransform): sx, sy = scale self._matrix = 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] + [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 else: @@ -366,7 +366,6 @@ class PiecewiseAffineTransform(ProjectiveTransform): affine.estimate(dst[tri, :], src[tri, :]) self.inverse_affines.append(affine) - def __call__(self, coords): """Apply forward transformation. @@ -992,7 +991,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, if orig_ndim == 2: out = out[..., 0] - if out is None: # use ndimage.map_coordinates + if out is None: # use ndimage.map_coordinates if output_shape is None: output_shape = ishape @@ -1018,5 +1017,5 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, if clipped.shape[0] == 1 or clipped.shape[1] == 1: return clipped - else: # remove singleton dim introduced by atleast_3d + else: # remove singleton dim introduced by atleast_3d return clipped.squeeze() diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index e8dc3ee7..b705ac47 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -130,7 +130,7 @@ def test_warp_coords_example(): assert 3 == image.shape[2] tform = SimilarityTransform(translation=(0, -10)) coords = warp_coords(tform, (30, 30, 3)) - warped_image1 = map_coordinates(image[:, :, 0], coords[:2]) + map_coordinates(image[:, :, 0], coords[:2]) if __name__ == "__main__": diff --git a/skimage/util/tests/test_dtype.py b/skimage/util/tests/test_dtype.py index 946a09eb..ae26cd27 100644 --- a/skimage/util/tests/test_dtype.py +++ b/skimage/util/tests/test_dtype.py @@ -1,7 +1,7 @@ import numpy as np from numpy.testing import assert_equal, assert_raises from skimage import img_as_int, img_as_float, \ - img_as_uint, img_as_ubyte, img_as_bool + img_as_uint, img_as_ubyte from skimage.util.dtype import convert @@ -92,7 +92,6 @@ def test_bool(): img8 = np.zeros((10, 10), np.bool8) img_[1, 1] = True img8[1, 1] = True - funcs = (img_as_float, img_as_int, img_as_ubyte, img_as_uint, img_as_bool) for (func, dt) in [(img_as_int, np.int16), (img_as_float, np.float64), (img_as_uint, np.uint16), diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index eaedb90c..69c9ebfb 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -9,6 +9,7 @@ __all__ = ['LineProfile'] #TODO: Extract line tool and add it to a new `canvastools` subpackage. + class LineProfile(PlotPlugin): """Plugin to compute interpolated intensity under a scan line on an image. @@ -59,7 +60,7 @@ class LineProfile(PlotPlugin): self.ax.set_ylim(self.limits) h, w = image.shape - self._init_end_pts = np.array([[w/3, h/2], [2*w/3, h/2]]) + self._init_end_pts = np.array([[w / 3, h / 2], [2 * w / 3, h / 2]]) self.end_pts = self._init_end_pts.copy() x, y = np.transpose(self.end_pts) @@ -99,14 +100,16 @@ class LineProfile(PlotPlugin): return end_pts, profile def on_scroll(self, event): - if not event.inaxes: return + if not event.inaxes: + return if event.button == 'up': self._thicken_scan_line() elif event.button == 'down': self._shrink_scan_line() def on_key_press(self, event): - if not event.inaxes: return + if not event.inaxes: + return elif event.key == '+': self._thicken_scan_line() elif event.key == '-': @@ -142,19 +145,25 @@ class LineProfile(PlotPlugin): return ind def on_mouse_press(self, event): - if event.button != 1: return - if event.inaxes==None: return + if event.button != 1: + return + if event.inaxes == None: + return self._active_pt = self.get_pt_under_cursor(event) def on_mouse_release(self, event): - if event.button != 1: return + if event.button != 1: + return self._active_pt = None def on_move(self, event): - if event.button != 1: return - if self._active_pt is None: return - if not self.image_viewer.ax.in_axes(event): return - x,y = event.xdata, event.ydata + if event.button != 1: + return + if self._active_pt is None: + return + if not self.image_viewer.ax.in_axes(event): + return + x, y = event.xdata, event.ydata self.line_changed(x, y) def reset(self): @@ -206,33 +215,33 @@ def profile_line(img, end_pts, linewidth=1): is the ceil of the computed length of the scan line. """ point1, point2 = end_pts - x1, y1 = point1 = np.asarray(point1, dtype = float) - x2, y2 = point2 = np.asarray(point2, dtype = float) + x1, y1 = point1 = np.asarray(point1, dtype=float) + x2, y2 = point2 = np.asarray(point2, dtype=float) dx, dy = point2 - point1 # Quick calculation if perfectly horizontal or vertical (remove?) if x1 == x2: - pixels = img[min(y1, y2) : max(y1, y2)+1, - x1 - linewidth / 2 : x1 + linewidth / 2 + 1] - intensities = pixels.mean(axis = 1) + pixels = img[min(y1, y2): max(y1, y2) + 1, + x1 - linewidth / 2: x1 + linewidth / 2 + 1] + intensities = pixels.mean(axis=1) return intensities elif y1 == y2: - pixels = img[y1 - linewidth / 2 : y1 + linewidth / 2 + 1, - min(x1, x2) : max(x1, x2)+1] - intensities = pixels.mean(axis = 0) + pixels = img[y1 - linewidth / 2: y1 + linewidth / 2 + 1, + min(x1, x2): max(x1, x2) + 1] + intensities = pixels.mean(axis=0) return intensities - theta = np.arctan2(dy,dx) - a = dy/dx + theta = np.arctan2(dy, dx) + a = dy / dx b = y1 - a * x1 length = np.hypot(dx, dy) line_x = np.linspace(min(x1, x2), max(x1, x2), np.ceil(length)) line_y = line_x * a + b - y_width = abs(linewidth * np.cos(theta)/2) + 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_xs = - a * perp_ys + (line_x + a * line_y)[:, np.newaxis] perp_lines = np.array([perp_ys, perp_xs]) pixels = ndi.map_coordinates(img, perp_lines) diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 74fd333f..cf632d5a 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -7,7 +7,7 @@ try: from matplotlib.colors import LinearSegmentedColormap from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg except ImportError: - FigureCanvasQTAgg = object # hack to prevent nosetest and autodoc errors + FigureCanvasQTAgg = object # hack to prevent nosetest and autodoc errors LinearSegmentedColormap = object print("Could not import matplotlib -- skimage.viewer not available.") @@ -33,6 +33,7 @@ def init_qtapp(): if QApp is None: QApp = QtGui.QApplication([]) + def start_qtapp(): """Start Qt mainloop""" QApp.exec_() @@ -100,8 +101,9 @@ class LinearColormap(LinearSegmentedColormap): segmented_data : dict Dictionary of 'red', 'green', 'blue', and (optionally) 'alpha' values. Each color key contains a list of `x`, `y` tuples. `x` must increase - monotonically from 0 to 1 and corresponds to input values for a mappable - object (e.g. an image). `y` corresponds to the color intensity. + monotonically from 0 to 1 and corresponds to input values for a + mappable object (e.g. an image). `y` corresponds to the color + intensity. """ def __init__(self, name, segmented_data, **kwargs): diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 7aadb571..e11967f7 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -5,7 +5,7 @@ try: from PyQt4 import QtGui, QtCore from PyQt4.QtGui import QMainWindow except ImportError: - QMainWindow = object # hack to prevent nosetest and autodoc errors + QMainWindow = object # hack to prevent nosetest and autodoc errors print("Could not import PyQt4 -- skimage.viewer not available.") from skimage.util.dtype import dtype_range @@ -16,7 +16,6 @@ from ..widgets import Slider __all__ = ['ImageViewer', 'CollectionViewer'] - class ImageCanvas(utils.MatplotlibCanvas): """Canvas for displaying images.""" def __init__(self, parent, image, **kwargs): @@ -233,7 +232,7 @@ class CollectionViewer(ImageViewer): first_image = image_collection[0] super(CollectionViewer, self).__init__(first_image) - slider_kws = dict(value=0, low=0, high=self.num_images-1) + slider_kws = dict(value=0, low=0, high=self.num_images - 1) slider_kws['update_on'] = update_on slider_kws['callback'] = self.update_index slider_kws['value_type'] = 'int' @@ -254,7 +253,7 @@ class CollectionViewer(ImageViewer): # clip index value to collection limits index = max(index, 0) - index = min(index, self.num_images-1) + index = min(index, self.num_images - 1) self.index = index self.slider.val = index diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 0ae2d1e8..9382652b 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -21,7 +21,7 @@ try: from PyQt4 import QtCore from PyQt4.QtGui import QWidget except ImportError: - QWidget = object # hack to prevent nosetest and autodoc errors + QWidget = object # hack to prevent nosetest and autodoc errors print("Could not import PyQt4 -- skimage.viewer not available.") from ..utils import RequiredAttr From a9d777a470504537cf7b8e3f4b47cf385be0246c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 29 Sep 2012 14:41:25 -0400 Subject: [PATCH 572/648] DOC: Add 0.7 release notes --- doc/release/release_0.7.txt | 69 +++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 doc/release/release_0.7.txt diff --git a/doc/release/release_0.7.txt b/doc/release/release_0.7.txt new file mode 100644 index 00000000..a6b591cb --- /dev/null +++ b/doc/release/release_0.7.txt @@ -0,0 +1,69 @@ +Announcement: scikits-image 0.7.0 +================================= + +We're happy to announce the 7th version of scikits-image! + +Scikits-image is an image processing toolbox for SciPy that includes algorithms +for segmentation, geometric transformations, color space manipulation, +analysis, filtering, morphology, feature detection, and more. + +For more information, examples, and documentation, please visit our website + + http://skimage.org + + +New Features +------------ + +It's been only 3 months since scikits-image 0.6 was released, but in that short +time, we've managed to add plenty of new features and enhancements, including + +- Geometric image transforms +- 3 new image segmentation routines (Felsenzwalb, Quickshift, SLIC) +- Local binary patterns for texture characterization +- Morphological reconstruction +- Polygon approximation +- CIE Lab color space conversion +- Image pyramids +- Multispectral support in random walker segmentation +- Slicing, concatenation, and natural sorting of image collections +- Perimeter and coordinates measurements in regionprops +- An extensible image viewer based on Qt and Matplotlib, with plugins for edge + detection, line-profiling, and viewing image collections + +Plus, this release adds a number of bug fixes, new examples, and performance +enhancements. + + +Contributors to this release +---------------------------- + +This release was only possible due to the efforts of many contributors, both +new and old. + +- Andreas Mueller +- Andreas Wuerl +- Andy Wilson +- Brian Holt +- Christoph Gohlke +- Dharhas Pothina +- Emmanuelle Gouillart +- Guillaume Gay +- Josh Warner +- James Bergstra +- Johannes Schonberger +- Jonathan J. Helmus +- Juan Nunez-Iglesias +- Leon Tietz +- Marianne Corvellec +- Matt McCormick +- Neil Yager +- Nicolas Pinto +- Nicolas Poilvert +- Pavel Campr +- Petter Strandmark +- Stefan van der Walt +- Tim Sheerman-Chase +- Tomas Kazmar +- Tony S Yu +- Wei Li From 37d9f566216b3936a96d9cec878d5eaa44fc14ab Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 29 Sep 2012 14:42:59 -0400 Subject: [PATCH 573/648] PKG: Update version to 0.7.0 --- bento.info | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bento.info b/bento.info index 4365cf04..affd2473 100644 --- a/bento.info +++ b/bento.info @@ -1,5 +1,5 @@ Name: scikits-image -Version: 0.7.0.dev0 +Version: 0.7.0 Summary: Image processing routines for SciPy Url: http://scikits-image.org DownloadUrl: http://github.com/scikits-image/scikits-image diff --git a/setup.py b/setup.py index b2f74c13..89e4a4ac 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ MAINTAINER_EMAIL = 'stefan@sun.ac.za' URL = 'http://scikits-image.org' LICENSE = 'Modified BSD' DOWNLOAD_URL = 'http://github.com/scikits-image/scikits-image' -VERSION = '0.7dev' +VERSION = '0.7.0' PYTHON_VERSION = (2, 5) DEPENDENCIES = { 'numpy': (1, 6), From 42c1bffcf791d14eb4d6d0f7ecae0144fda2b13c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 29 Sep 2012 14:44:37 -0400 Subject: [PATCH 574/648] PKG: Update docversions.js to 0.7 --- doc/source/themes/agogo/static/docversions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/themes/agogo/static/docversions.js b/doc/source/themes/agogo/static/docversions.js index d0144ee4..800d5754 100644 --- a/doc/source/themes/agogo/static/docversions.js +++ b/doc/source/themes/agogo/static/docversions.js @@ -1,5 +1,5 @@ function insert_version_links() { - var labels = ['dev', '0.6', '0.5', '0.4', '0.3']; + var labels = ['dev', '0.7', '0.6', '0.5', '0.4', '0.3']; document.write('