diff --git a/skimage/feature/tests/__init__.py b/skimage/feature/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/skimage/feature/tests/test_match.py b/skimage/feature/tests/test_match.py index 1b0a622f..695116ed 100644 --- a/skimage/feature/tests/test_match.py +++ b/skimage/feature/tests/test_match.py @@ -32,7 +32,7 @@ def test_binary_descriptors_lena_rotation_crosscheck_false(): img = data.lena() img = rgb2gray(img) tform = tf.SimilarityTransform(scale=1, rotation=0.15, translation=(0, 0)) - rotated_img = tf.warp(img, tform) + rotated_img = tf.warp(img, tform, clip=False) extractor = BRIEF(descriptor_size=512) @@ -65,7 +65,7 @@ def test_binary_descriptors_lena_rotation_crosscheck_true(): img = data.lena() img = rgb2gray(img) tform = tf.SimilarityTransform(scale=1, rotation=0.15, translation=(0, 0)) - rotated_img = tf.warp(img, tform) + rotated_img = tf.warp(img, tform, clip=False) extractor = BRIEF(descriptor_size=512) diff --git a/skimage/feature/tests/test_orb.py b/skimage/feature/tests/test_orb.py index 30394d07..5bed2bf3 100644 --- a/skimage/feature/tests/test_orb.py +++ b/skimage/feature/tests/test_orb.py @@ -1,11 +1,11 @@ import numpy as np -from numpy.testing import assert_array_equal, assert_almost_equal +from numpy.testing import assert_equal, assert_almost_equal, run_module_suite from skimage.feature import ORB -from skimage.data import lena +from skimage import data from skimage.color import rgb2gray -img = rgb2gray(lena()) +img = rgb2gray(data.lena()) def test_keypoints_orb_desired_no_of_keypoints(): @@ -42,7 +42,6 @@ def test_keypoints_orb_desired_no_of_keypoints(): def test_keypoints_orb_less_than_desired_no_of_keypoints(): - img = rgb2gray(lena()) detector_extractor = ORB(n_keypoints=15, fast_n=12, fast_threshold=0.33, downscale=2, n_scales=2) detector_extractor.detect(img) @@ -102,14 +101,13 @@ def test_descriptor_orb(): detector_extractor.extract(img, detector_extractor.keypoints, detector_extractor.scales, detector_extractor.orientations) - assert_array_equal(exp_descriptors, - detector_extractor.descriptors[100:120, 10:20]) + assert_equal(exp_descriptors, + detector_extractor.descriptors[100:120, 10:20]) detector_extractor.detect_and_extract(img) - assert_array_equal(exp_descriptors, - detector_extractor.descriptors[100:120, 10:20]) + assert_equal(exp_descriptors, + detector_extractor.descriptors[100:120, 10:20]) if __name__ == '__main__': - from numpy import testing - testing.run_module_suite() + run_module_suite() diff --git a/skimage/novice/_novice.py b/skimage/novice/_novice.py index c8fd9284..03422b2e 100644 --- a/skimage/novice/_novice.py +++ b/skimage/novice/_novice.py @@ -353,8 +353,9 @@ class Picture(object): if (value[0] != self.width) or (value[1] != self.height): # skimage dimensions are flipped: y, x new_size = (int(value[1]), int(value[0])) - new_array = resize(self.array, new_size, order=0) - self.array = img_as_ubyte(new_array) + new_array = resize(self.array, new_size, order=0, + preserve_range=True) + self.array = new_array.astype(np.uint8) self._array_modified() diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 456570f2..e658d23e 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -4,8 +4,9 @@ import warnings import numpy as np from scipy import ndimage, spatial -from skimage._shared.utils import get_bound_method_class, safe_as_int -from skimage.util import img_as_float +from .._shared.utils import get_bound_method_class, safe_as_int +from ..util import img_as_float +from ..exposure import rescale_intensity from ._warps_cy import _warp_fast @@ -994,8 +995,64 @@ def warp_coords(coord_map, shape, dtype=np.float64): return coords +def _convert_warp_input(image, preserve_range): + """Convert input image to double image with the appropriate range.""" + if preserve_range: + image = image.astype(np.double) + else: + image = img_as_float(image) + return image + + +def _clip_warp_output(input_image, output_image, order, mode, cval, clip): + """Clip output image to range of values of input image. + + Note that this function modifies the values of `output_image` in-place + and it is only modified if ``clip=True``. + + Parameters + ---------- + input_image : ndarray + Input image. + output_image : ndarray + Output image, which is modified in-place. + + Other parameters + ---------------- + order : int, optional + The order of the spline interpolation, default is 1. The order has to + be in the range 0-5. See `skimage.transform.warp` for detail. + mode : string, optional + Points outside the boundaries of the input are filled according + to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + + """ + + if clip and order != 0: + min_val = input_image.min() + max_val = input_image.max() + + preserve_cval = mode == 'constant' and not \ + (min_val <= cval <= max_val) + + if preserve_cval: + cval_mask = output_image == cval + + np.clip(output_image, min_val, max_val, out=output_image) + + if preserve_cval: + output_image[cval_mask] = cval + + def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, - mode='constant', cval=0., clip=True): + mode='constant', cval=0., clip=True, preserve_range=False): """Warp an image according to a given coordinate transformation. Parameters @@ -1055,17 +1112,25 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, Used in conjunction with mode 'constant', the value outside the image boundaries. clip : bool, optional - Whether to clip the output to the float range of ``[0, 1]``, or - ``[-1, 1]`` for input images with negative values. This is enabled by - default, since higher order interpolation may produce values outside - the given input range. + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + + Returns + ------- + warped : double ndarray + The warped input image. Notes ----- - In case of a `SimilarityTransform`, `AffineTransform` and - `ProjectiveTransform` and `order` in [0, 3] this function uses the - underlying transformation matrix to warp the image with a much faster - routine. + - The input image is converted to a `double` image. + - In case of a `SimilarityTransform`, `AffineTransform` and + `ProjectiveTransform` and `order` in [0, 3] this function uses the + underlying transformation matrix to warp the image with a much faster + routine. Examples -------- @@ -1124,7 +1189,8 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, """ - image = img_as_float(image) + image = _convert_warp_input(image, preserve_range) + input_shape = np.array(image.shape) if output_shape is None: @@ -1132,7 +1198,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, else: output_shape = safe_as_int(output_shape) - out = None + warped = None if order == 2: # When fixing this issue, make sure to fix the branches further @@ -1168,7 +1234,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, if matrix is not None: matrix = matrix.astype(np.double) if image.ndim == 2: - out = _warp_fast(image, matrix, + warped = _warp_fast(image, matrix, output_shape=output_shape, order=order, mode=mode, cval=cval) elif image.ndim == 3: @@ -1177,9 +1243,9 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, dims.append(_warp_fast(image[..., dim], matrix, output_shape=output_shape, order=order, mode=mode, cval=cval)) - out = np.dstack(dims) + warped = np.dstack(dims) - if out is None: + if warped is None: # use ndimage.map_coordinates if (isinstance(inverse_map, np.ndarray) @@ -1216,24 +1282,10 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # Pre-filtering not necessary for order 0, 1 interpolation prefilter = order > 1 - out = ndimage.map_coordinates(image, coords, prefilter=prefilter, + warped = ndimage.map_coordinates(image, coords, prefilter=prefilter, mode=mode, order=order, cval=cval) - if clip: - # The spline filters sometimes return results outside [0, 1], - # so clip to ensure valid data - if np.min(image) < 0: - min_val = -1 - else: - min_val = 0 - max_val = 1 + _clip_warp_output(image, warped, order, mode, cval, clip) - clipped = np.clip(out, min_val, max_val) - - if mode == 'constant' and not (0 <= cval <= 1): - clipped[out == cval] = cval - - out = clipped - - return out + return warped diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 4edefe73..a27c1d42 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -1,12 +1,13 @@ import numpy as np from scipy import ndimage -from skimage.transform._geometric import (warp, SimilarityTransform, - AffineTransform) -from skimage.measure import block_reduce +from ..measure import block_reduce +from ._geometric import (warp, SimilarityTransform, AffineTransform, + _convert_warp_input, _clip_warp_output) -def resize(image, output_shape, order=1, mode='constant', cval=0.): +def resize(image, output_shape, order=1, mode='constant', cval=0, clip=True, + preserve_range=False): """Resize image to match a certain size. Performs interpolation to up-size or down-size images. For down-sampling @@ -40,6 +41,13 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. Examples -------- @@ -71,8 +79,12 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): coord_map = np.array([map_rows, map_cols, map_dims]) - out = ndimage.map_coordinates(image, coord_map, order=order, mode=mode, - cval=cval) + image = _convert_warp_input(image, preserve_range) + + out = ndimage.map_coordinates(image, coord_map, order=order, + mode=mode, cval=cval) + + _clip_warp_output(image, out, order, mode, cval, clip) else: # 2-dimensional interpolation @@ -87,12 +99,13 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): tform.estimate(src_corners, dst_corners) out = warp(image, tform, output_shape=output_shape, order=order, - mode=mode, cval=cval) + mode=mode, cval=cval, clip=clip, preserve_range=preserve_range) return out -def rescale(image, scale, order=1, mode='constant', cval=0.): +def rescale(image, scale, order=1, mode='constant', cval=0, clip=True, + preserve_range=False): """Scale image by a certain factor. Performs interpolation to upscale or down-scale images. For down-sampling @@ -124,6 +137,13 @@ def rescale(image, scale, order=1, mode='constant', cval=0.): cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. Examples -------- @@ -147,11 +167,12 @@ def rescale(image, scale, order=1, mode='constant', cval=0.): cols = np.round(col_scale * orig_cols) output_shape = (rows, cols) - return resize(image, output_shape, order=order, mode=mode, cval=cval) + return resize(image, output_shape, order=order, mode=mode, cval=cval, + clip=clip, preserve_range=preserve_range) -def rotate(image, angle, resize=False, order=1, mode='constant', cval=0., - center=None): +def rotate(image, angle, resize=False, center=None, order=1, mode='constant', + cval=0, clip=True, preserve_range=False): """Rotate image by a certain angle around its center. Parameters @@ -164,6 +185,9 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0., Determine whether the shape of the output image will be automatically calculated, so the complete rotated image exactly fits. Default is False. + center : iterable of length 2 + The rotation center. If ``center=None``, the image is rotated around + its center, i.e. ``center=(rows / 2 - 0.5, cols / 2 - 0.5)``. Returns ------- @@ -181,9 +205,13 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0., cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. - center : iterable of length 2 - The rotation center. If ``center=None``, the image is rotated around - its center, i.e. ``center=(rows / 2 - 0.5, cols / 2 - 0.5)``. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. Examples -------- @@ -230,10 +258,10 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0., tform = tform4 + tform return warp(image, tform, output_shape=output_shape, order=order, - mode=mode, cval=cval) + mode=mode, cval=cval, clip=clip, preserve_range=preserve_range) -def downscale_local_mean(image, factors, cval=0): +def downscale_local_mean(image, factors, cval=0, clip=True): """Down-sample N-dimensional image by local averaging. The image is padded with `cval` if it is not perfectly divisible by the @@ -294,7 +322,8 @@ def _swirl_mapping(xy, center, rotation, strength, radius): def swirl(image, center=None, strength=1, radius=100, rotation=0, - output_shape=None, order=1, mode='constant', cval=0): + output_shape=None, order=1, mode='constant', cval=0, clip=True, + preserve_range=False): """Perform a swirl transformation. Parameters @@ -330,6 +359,13 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. """ @@ -342,5 +378,5 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, 'radius': radius} return warp(image, _swirl_mapping, map_args=warp_args, - output_shape=output_shape, - order=order, mode=mode, cval=cval) + output_shape=output_shape, order=order, mode=mode, cval=cval, + clip=clip, preserve_range=preserve_range) diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index 433c586d..47a28e35 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -3,7 +3,6 @@ #cython: nonecheck=False #cython: wraparound=False import numpy as np - cimport numpy as cnp from skimage._shared.interpolation cimport (nearest_neighbour_interpolation, bilinear_interpolation, diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index a2d6265f..09a93d69 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -1,5 +1,5 @@ from numpy.testing import (assert_almost_equal, run_module_suite, - assert_array_equal, assert_raises) + assert_equal, assert_raises) import numpy as np from scipy.ndimage import map_coordinates @@ -75,14 +75,15 @@ def test_warp_nd(): def test_warp_clip(): - x = 2 * np.ones((5, 5), dtype=np.double) - matrix = np.eye(3) + x = np.zeros((5, 5), dtype=np.double) + x[2, 2] = 1 - outx = warp(x, matrix, order=0, clip=False) - assert_almost_equal(x, outx) + outx = rescale(x, 3, order=3, clip=False) + assert outx.min() < 0 - outx = warp(x, matrix, order=0, clip=True) - assert_almost_equal(x / 2, outx) + outx = rescale(x, 3, order=3, clip=True) + assert_almost_equal(outx.min(), 0) + assert_almost_equal(outx.max(), 1) def test_homography(): @@ -235,17 +236,16 @@ def test_downscale_local_mean(): out1 = downscale_local_mean(image1, (2, 3)) expected1 = np.array([[ 4., 7.], [ 16., 19.]]) - assert_array_equal(expected1, out1) + assert_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) out2 = downscale_local_mean(image2, (4, 5)) expected2 = np.array([[ 14. , 10.8], [ 8.5, 5.7]]) - assert_array_equal(expected2, out2) + assert_equal(expected2, out2) def test_invalid(): - assert_raises(ValueError, warp, np.ones((4, )), SimilarityTransform()) assert_raises(ValueError, warp, np.ones((4, 3, 3, 3)), SimilarityTransform()) @@ -254,7 +254,7 @@ def test_inverse(): tform = SimilarityTransform(scale=0.5, rotation=0.1) inverse_tform = SimilarityTransform(matrix=np.linalg.inv(tform.params)) image = np.arange(10 * 10).reshape(10, 10).astype(np.double) - assert_array_equal(warp(image, inverse_tform), warp(image, tform.inverse)) + assert_equal(warp(image, inverse_tform), warp(image, tform.inverse)) def test_slow_warp_nonint_oshape(): @@ -266,5 +266,23 @@ def test_slow_warp_nonint_oshape(): warp(image, lambda xy: xy, output_shape=(13.0001, 19.9999)) +def test_keep_range(): + image = np.linspace(0, 2, 25).reshape(5, 5) + + out = rescale(image, 2, preserve_range=False, clip=True, order=0) + assert out.min() == 0 + assert out.max() == 2 + + out = rescale(image, 2, preserve_range=True, clip=True, order=0) + assert out.min() == 0 + assert out.max() == 2 + + out = rescale(image.astype(np.uint8), 2, preserve_range=False, + clip=True, order=0) + assert out.min() == 0 + assert out.max() == 2 / 255.0 + + + if __name__ == "__main__": run_module_suite()