From d30ed259688bd06c3ec4038f8739f9fb407327df Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 28 Mar 2015 21:04:21 +0530 Subject: [PATCH 01/26] Added implementation and doc string --- doc/examples/plot_seam_carving.py | 19 +++ skimage/transform/__init__.py | 4 +- skimage/transform/_seam_carving.pyx | 210 ++++++++++++++++++++++++++++ skimage/transform/seam_carving.py | 73 ++++++++++ skimage/transform/setup.py | 3 + 5 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 doc/examples/plot_seam_carving.py create mode 100644 skimage/transform/_seam_carving.pyx create mode 100644 skimage/transform/seam_carving.py diff --git a/doc/examples/plot_seam_carving.py b/doc/examples/plot_seam_carving.py new file mode 100644 index 00000000..1c2dcfc9 --- /dev/null +++ b/doc/examples/plot_seam_carving.py @@ -0,0 +1,19 @@ +from skimage import io, data +from skimage import transform +from skimage import color, filters +from matplotlib import pyplot as plt + +def custom_sobel(img): + if img.ndim == 3: + img = color.rgb2gray(img) + + return filters.sobel(img) + +img = data.coins() +out = transform.seam_carve(img, 'vertical', 80, energy_func = custom_sobel) +out = transform.seam_carve(out, 'horizontal', 70, energy_func = custom_sobel) + +io.imshow(out) +plt.figure() +io.imshow(img) +io.show() diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 3049ebb0..05484887 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -12,6 +12,7 @@ from ._geometric import (warp, warp_coords, estimate_transform, from ._warps import swirl, resize, rotate, rescale, downscale_local_mean from .pyramids import (pyramid_reduce, pyramid_expand, pyramid_gaussian, pyramid_laplacian) +from seam_carving import seam_carve __all__ = ['hough_circle', @@ -43,4 +44,5 @@ __all__ = ['hough_circle', 'pyramid_reduce', 'pyramid_expand', 'pyramid_gaussian', - 'pyramid_laplacian'] + 'pyramid_laplacian', + 'seam_carve'] diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx new file mode 100644 index 00000000..3d07beb5 --- /dev/null +++ b/skimage/transform/_seam_carving.pyx @@ -0,0 +1,210 @@ +# cython: cdivision=True +# cython: boundscheck=False +# cython: nonecheck=False +# cython: wraparound=False +import numpy as np +cimport numpy as cnp + + +cdef cnp.double_t ABSOLUTE_MAX = np.finfo(np.double).max + + +cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, + cnp.double_t[::1] current_cost, cnp.double_t[::1] prev_cost, + Py_ssize_t cols): + """Find a single vertical seam in an image that will be removed. + + Parameters + ---------- + energy_img : (M, N) ndarray + The energy image where a higher value signifies a pixel of more + importance. + track_img : (M, N) ndarray + The image used to store the optimal decision made at each point while + finding a minimum cost path. + current_cost : (N, ) ndarray + An array to store the current cost of the optimal path for each column + in row currently being processed. + prev_cost : (N, ) ndarray + An array to store the current cost of the optimal path for each column + in row prior to the one being processed. + cols : int + The number of cols to process for seam carving. Columns with indices + more than `cols` are ignored. + + + Returns + ------- + seam : (M, ) ndarray + An array containing the index of the row of the pixel to be removed + for each column in the image. + + Notes + ----- + `track_img`, `current_cost` and `prev_cost` are passed as arguments to + avoid memory allocation at each iteration of `_seam_carve_v`. + """ + + cdef Py_ssize_t rows, row, col + rows = energy_img.shape[0] + cdef cnp.double_t tmp, min_cost + cdef Py_ssize_t offset, idx, offset_clip + + cdef Py_ssize_t[::1] seam = np.zeros(rows, dtype=np.int) + + for idx in range(cols): + prev_cost[idx] = energy_img[0, idx] + + for row in range(1, rows): + for col in range(0, cols): + + min_cost = ABSOLUTE_MAX + for offset in range(-1, 2): + idx = col + offset + + if idx > cols - 1 or idx < 0: + continue + + if prev_cost[idx] < min_cost: + min_cost = prev_cost[idx] + track_img[row, col] = offset + + current_cost[col] = min_cost + energy_img[row, col] + + prev_cost[:] = current_cost + + seam[rows-1] = np.argmin(current_cost) + + for row in range(rows-2, -1, -1): + col = seam[row + 1] + offset = track_img[row, col] + #print offset + seam[row] = seam[row + 1] + offset + + return seam + + +cdef remove_seam_h_2d(cnp.double_t[:, ::1] img, Py_ssize_t[::1] seam, + Py_ssize_t cols): + cdef Py_ssize_t rows, row, col, idx + rows = img.shape[0] + """ Removes one horizontal seam from the image. + + The method modifies `img` so that all pixels to the right of the vertical + seam are pushed one place left. + + image : (M, N) ndarray + Input image whose vertical seam is to be removed. + seam : (M, ) ndarray + An array use to store the index of the column in the seam for each row. + cols : int + Number of columns in the input image to process. Column indices more + than `cols` are ingored. + + Notes + ----- + `seam` is passed as an argument so that we don't have to reallocate it for + each iteration in `_seam_carve_v`. + """" + + for row in range(rows): + for idx in range(seam[row], cols - 1): + img[row, idx] = img[row, idx + 1] + + +cdef remove_seam_h_3d(cnp.double_t[:, :, ::1] img, Py_ssize_t[::1] seam, + Py_ssize_t cols): + """ Removes one horizontal seam from the image. + + The method modifies `img` so that all pixels to the right of the vertical + seam are pushed one place left. + + image : (M, N, 3) ndarray + Input image whose vertical seam is to be removed. + seam : (M, ) ndarray + An array use to store the index of the column in the seam for each row. + cols : int + Number of columns in the input image to process. Column indices more + than `cols` are ingored. + + Notes + ----- + `seam` is passed as an argument so that we don't have to reallocate it for + each iteration in `_seam_carve_v`. + """" + cdef Py_ssize_t rows, row, col, idx + rows = img.shape[0] + + for row in range(rows): + for idx in range(seam[row], cols - 1): + img[row, idx, :] = img[row, idx + 1, :] + + +def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): + """ Carve vertical seams off an image. + + Carves out vertical seams off an image while using the given energy + function to decide the importance of each pixel.[1] + + Parameters + ---------- + image : (M, N) or (M, N, 3) ndarray + Input image whose vertical seams are to be removed. + iters : int + Number of vertical seams are to be removed. + energy_func : callable + The function used to decide the importance of each pixel. The higher + the value corresponding to a pixel, the more the algorithm will try + to keep it in the image. For every iteration `energy_func` is called + as `energy_func(image, *extra_args, **extra_kwargs)`, where `image` + is the cropped image during each iteration and is expected to return a + (M, N) ndarray depicting each pixel's importance. + extra_args : iterable + The extra arguments supplied to `energy_func`. + extra_kwargs : dict + The extra keyword arguments supplied to `energy_func`. + border : int + The number of pixels in the right and left end of the image to be + excluded from being considered for a seam. This is important as certain + filters just ignore image boundaries and set them to `0`. + + Returns + ------- + image : (M, N - iters) or (M, N - iters, 3) ndarray + The cropped image with the vertical seams removed. + + References + ---------- + .. [1] Shai Avidan and Ariel Shamir + "Seam Carving for Content-Aware Image Resizing" + http://www.cs.jhu.edu/~misha/ReadingSeminar/Papers/Avidan07.pdf + """ + cdef Py_ssize_t[::1] seam + cdef Py_ssize_t ndim = img.ndim + cdef Py_ssize_t cols = img.shape[1] + + track_img = np.zeros(img.shape[0:2], dtype=np.int8) + + current_cost = np.zeros_like(track_img[0], dtype = img.dtype) + prev_cost = np.zeros_like(track_img[0], dtype = img.dtype) + + for i in range(iters): + + sliced_img = img[:, 0:cols] + energy_img = energy_func(sliced_img, *extra_args, **extra_kwargs) + + # So that borders are ignored. + energy_img[:, 0:border] = ABSOLUTE_MAX + energy_img[:, cols-border:cols] = ABSOLUTE_MAX + + seam = find_seam_v(energy_img, track_img, current_cost, prev_cost, + cols) + + if ndim == 2: + remove_seam_h_2d(img, seam, cols) + elif ndim == 3: + remove_seam_h_3d(img, seam, cols) + + cols -= 1 + + return img[:, 0:cols] diff --git a/skimage/transform/seam_carving.py b/skimage/transform/seam_carving.py new file mode 100644 index 00000000..c716c0d1 --- /dev/null +++ b/skimage/transform/seam_carving.py @@ -0,0 +1,73 @@ +from _seam_carving import _seam_carve_h +from ..import filters +from .. import util +from .._shared import utils +import numpy as np + + +def seam_carve(img, mode, num, energy_func, extra_args = [], + extra_kwargs = {}, border=1, force_copy = True): + """ Carve vertical or horizontal seams off an image. + + Carves out vertical/horizontal seams off an image while using the given + energy function to decide the importance of each pixel. + + Parameters + ---------- + image : (M, N) or (M, N, 3) ndarray + Input image whose vertical seams are to be removed. + mode : str {'horizontal', 'vertical'} + Indicates whether seams are to be removed vertically or horizontally. + Removing seams horizontally will decrease the height whereas removing + vertically will decrease the width. + num : int + Number of seams are to be removed. + energy_func : callable + The function used to decide the importance of each pixel. The higher + the value corresponding to a pixel, the more the algorithm will try + to keep it in the image. For every iteration `energy_func` is called + as `energy_func(image, *extra_args, **extra_kwargs)`, where `image` + is the cropped image during each iteration and is expected to return a + (M, N) ndarray depicting each pixel's importance. + extra_args : iterable, optional + The extra arguments supplied to `energy_func`. + extra_kwargs : dict, optional + The extra keyword arguments supplied to `energy_func`. + border : int, optional + The number of pixels in the right and left end of the image to be + excluded from being considered for a seam. This is important as certain + filters just ignore image boundaries and set them to `0`. By default + border is set to `1`. + force_copy : bool, optional + If set, the image is copied before being used by the method which + modifies it in place. Set this to `False` if the original image is no + loner needed after this opetration. + + Returns + ------- + out : ndarray + The cropped image with the seams removed. + """ + + utils.assert_nD(img, (2,3)) + img = util.img_as_float(img) + + + if mode == 'horizontal': + img = np.ascontiguousarray(img) + return _seam_carve_h(img, num, energy_func, extra_args ,extra_kwargs, + border) + elif mode == 'vertical' : + if img.ndim == 3: + img = np.transpose(img, (1, 0, 2)) + else: + img = img.T + + img = np.ascontiguousarray(img) + out = _seam_carve_h(img, num, energy_func, extra_args , extra_kwargs, + border) + + if img.ndim == 3: + return np.transpose(out, (1, 0, 2)) + else: + return out.T diff --git a/skimage/transform/setup.py b/skimage/transform/setup.py index 22f31696..ff2e9bc6 100644 --- a/skimage/transform/setup.py +++ b/skimage/transform/setup.py @@ -16,6 +16,7 @@ def configuration(parent_package='', top_path=None): cython(['_hough_transform.pyx'], working_path=base_path) cython(['_warps_cy.pyx'], working_path=base_path) cython(['_radon_transform.pyx'], working_path=base_path) + cython(['_seam_carving.pyx'], working_path=base_path) config.add_extension('_hough_transform', sources=['_hough_transform.c'], include_dirs=[get_numpy_include_dirs()]) @@ -27,6 +28,8 @@ def configuration(parent_package='', top_path=None): sources=['_radon_transform.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_seam_carving', sources=['_seam_carving.c'], + include_dirs=[get_numpy_include_dirs()]) return config if __name__ == '__main__': From 29ec5ee3ecd7dac999cbbc49d149978c520f9ab9 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sun, 29 Mar 2015 15:25:08 +0530 Subject: [PATCH 02/26] Added example and test --- doc/examples/plot_seam_carving.py | 37 +++++++++++++++----- skimage/transform/_seam_carving.pyx | 12 +++---- skimage/transform/seam_carving.py | 12 +++++-- skimage/transform/tests/test_seam_carving.py | 33 +++++++++++++++++ 4 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 skimage/transform/tests/test_seam_carving.py diff --git a/doc/examples/plot_seam_carving.py b/doc/examples/plot_seam_carving.py index 1c2dcfc9..0583cd5d 100644 --- a/doc/examples/plot_seam_carving.py +++ b/doc/examples/plot_seam_carving.py @@ -1,19 +1,38 @@ +""" +============ +Seam Carving +============ + +This example demonstrates how images can be resized using seam carving [1]_. +Resizing often distorts contents in the image. Seam carving tries to resize +images while trying to keep important content intact. In this example we are +using the Sobel filter to signify the importance of each pixel. + +.. [1] Shai Avidan and Ariel Shamir + "Seam Carving for Content-Aware Image Resizing" + http://www.cs.jhu.edu/~misha/ReadingSeminar/Papers/Avidan07.pdf + +""" from skimage import io, data from skimage import transform from skimage import color, filters from matplotlib import pyplot as plt -def custom_sobel(img): - if img.ndim == 3: - img = color.rgb2gray(img) - - return filters.sobel(img) img = data.coins() -out = transform.seam_carve(img, 'vertical', 80, energy_func = custom_sobel) -out = transform.seam_carve(out, 'horizontal', 70, energy_func = custom_sobel) +out = transform.seam_carve(img, 'vertical', 80, energy_func = filters.sobel) +out = transform.seam_carve(out, 'horizontal', 70, energy_func = filters.sobel) +resized = transform.resize(img, out.shape) + +plt.title('Original Image') +io.imshow(img, plugin='matplotlib') -io.imshow(out) plt.figure() -io.imshow(img) +plt.title('Resized Image Image') +io.imshow(resized, plugin='matplotlib') + +plt.figure() +plt.title('Resized Image Image') +io.imshow(out, plugin='matplotlib') + io.show() diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index 3d07beb5..188c8dcb 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -84,7 +84,7 @@ cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, return seam -cdef remove_seam_h_2d(cnp.double_t[:, ::1] img, Py_ssize_t[::1] seam, +cdef remove_seam_v_2d(cnp.double_t[:, ::1] img, Py_ssize_t[::1] seam, Py_ssize_t cols): cdef Py_ssize_t rows, row, col, idx rows = img.shape[0] @@ -105,14 +105,14 @@ cdef remove_seam_h_2d(cnp.double_t[:, ::1] img, Py_ssize_t[::1] seam, ----- `seam` is passed as an argument so that we don't have to reallocate it for each iteration in `_seam_carve_v`. - """" + """ for row in range(rows): for idx in range(seam[row], cols - 1): img[row, idx] = img[row, idx + 1] -cdef remove_seam_h_3d(cnp.double_t[:, :, ::1] img, Py_ssize_t[::1] seam, +cdef remove_seam_v_3d(cnp.double_t[:, :, ::1] img, Py_ssize_t[::1] seam, Py_ssize_t cols): """ Removes one horizontal seam from the image. @@ -131,7 +131,7 @@ cdef remove_seam_h_3d(cnp.double_t[:, :, ::1] img, Py_ssize_t[::1] seam, ----- `seam` is passed as an argument so that we don't have to reallocate it for each iteration in `_seam_carve_v`. - """" + """ cdef Py_ssize_t rows, row, col, idx rows = img.shape[0] @@ -201,9 +201,9 @@ def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): cols) if ndim == 2: - remove_seam_h_2d(img, seam, cols) + remove_seam_v_2d(img, seam, cols) elif ndim == 3: - remove_seam_h_3d(img, seam, cols) + remove_seam_v_3d(img, seam, cols) cols -= 1 diff --git a/skimage/transform/seam_carving.py b/skimage/transform/seam_carving.py index c716c0d1..e9c700b0 100644 --- a/skimage/transform/seam_carving.py +++ b/skimage/transform/seam_carving.py @@ -1,4 +1,4 @@ -from _seam_carving import _seam_carve_h +from _seam_carving import _seam_carve_v from ..import filters from .. import util from .._shared import utils @@ -47,6 +47,12 @@ def seam_carve(img, mode, num, energy_func, extra_args = [], ------- out : ndarray The cropped image with the seams removed. + + References + ---------- + .. [1] Shai Avidan and Ariel Shamir + "Seam Carving for Content-Aware Image Resizing" + http://www.cs.jhu.edu/~misha/ReadingSeminar/Papers/Avidan07.pdf """ utils.assert_nD(img, (2,3)) @@ -55,7 +61,7 @@ def seam_carve(img, mode, num, energy_func, extra_args = [], if mode == 'horizontal': img = np.ascontiguousarray(img) - return _seam_carve_h(img, num, energy_func, extra_args ,extra_kwargs, + return _seam_carve_v(img, num, energy_func, extra_args ,extra_kwargs, border) elif mode == 'vertical' : if img.ndim == 3: @@ -64,7 +70,7 @@ def seam_carve(img, mode, num, energy_func, extra_args = [], img = img.T img = np.ascontiguousarray(img) - out = _seam_carve_h(img, num, energy_func, extra_args , extra_kwargs, + out = _seam_carve_v(img, num, energy_func, extra_args , extra_kwargs, border) if img.ndim == 3: diff --git a/skimage/transform/tests/test_seam_carving.py b/skimage/transform/tests/test_seam_carving.py new file mode 100644 index 00000000..0c476b2d --- /dev/null +++ b/skimage/transform/tests/test_seam_carving.py @@ -0,0 +1,33 @@ +from skimage import transform +import numpy as np +from numpy import testing + +def energy(img): + if(img.ndim == 3): + return np.ascontiguousarray(img[:, :, 0]) + return (1 - img) + +def test_seam_carving(): + img = np.array([[0, 0, 1, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 1, 0, 0], + [0, 1, 0, 0, 0], + [1, 0 , 0, 0, 0]], dtype = np.float ) + + out = transform.seam_carve(img, 'horizontal', 1, energy, border=0) + testing.assert_allclose(out, 0) + + img = img.T + out = transform.seam_carve(img, 'vertical', 1, energy, border=0) + testing.assert_allclose(out, 0) + + img = img.T + + img3 = np.dstack([img, img, img]) + + out = transform.seam_carve(img3, 'horizontal', 1, energy, border=0) + testing.assert_allclose(out, 0) + + + out = transform.seam_carve(img3, 'vertical', 1, energy, border=0) + testing.assert_allclose(out, 0) From 1f570a145f9a61d25f025db1ec42dc66d109ac5c Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sun, 29 Mar 2015 15:34:06 +0530 Subject: [PATCH 03/26] Formatting changes --- skimage/transform/seam_carving.py | 16 +++++++--------- skimage/transform/tests/test_seam_carving.py | 5 +++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/skimage/transform/seam_carving.py b/skimage/transform/seam_carving.py index e9c700b0..0777e377 100644 --- a/skimage/transform/seam_carving.py +++ b/skimage/transform/seam_carving.py @@ -1,12 +1,11 @@ from _seam_carving import _seam_carve_v -from ..import filters from .. import util from .._shared import utils import numpy as np -def seam_carve(img, mode, num, energy_func, extra_args = [], - extra_kwargs = {}, border=1, force_copy = True): +def seam_carve(img, mode, num, energy_func, extra_args=[], + extra_kwargs={}, border=1, force_copy=True): """ Carve vertical or horizontal seams off an image. Carves out vertical/horizontal seams off an image while using the given @@ -55,25 +54,24 @@ def seam_carve(img, mode, num, energy_func, extra_args = [], http://www.cs.jhu.edu/~misha/ReadingSeminar/Papers/Avidan07.pdf """ - utils.assert_nD(img, (2,3)) + utils.assert_nD(img, (2, 3)) img = util.img_as_float(img) - if mode == 'horizontal': img = np.ascontiguousarray(img) - return _seam_carve_v(img, num, energy_func, extra_args ,extra_kwargs, + return _seam_carve_v(img, num, energy_func, extra_args, extra_kwargs, border) - elif mode == 'vertical' : + elif mode == 'vertical': if img.ndim == 3: img = np.transpose(img, (1, 0, 2)) else: img = img.T img = np.ascontiguousarray(img) - out = _seam_carve_v(img, num, energy_func, extra_args , extra_kwargs, + out = _seam_carve_v(img, num, energy_func, extra_args, extra_kwargs, border) if img.ndim == 3: - return np.transpose(out, (1, 0, 2)) + return np.transpose(out, (1, 0, 2)) else: return out.T diff --git a/skimage/transform/tests/test_seam_carving.py b/skimage/transform/tests/test_seam_carving.py index 0c476b2d..d17eee25 100644 --- a/skimage/transform/tests/test_seam_carving.py +++ b/skimage/transform/tests/test_seam_carving.py @@ -2,17 +2,19 @@ from skimage import transform import numpy as np from numpy import testing + def energy(img): if(img.ndim == 3): return np.ascontiguousarray(img[:, :, 0]) return (1 - img) + def test_seam_carving(): img = np.array([[0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 1, 0, 0, 0], - [1, 0 , 0, 0, 0]], dtype = np.float ) + [1, 0, 0, 0, 0]], dtype=np.float) out = transform.seam_carve(img, 'horizontal', 1, energy, border=0) testing.assert_allclose(out, 0) @@ -28,6 +30,5 @@ def test_seam_carving(): out = transform.seam_carve(img3, 'horizontal', 1, energy, border=0) testing.assert_allclose(out, 0) - out = transform.seam_carve(img3, 'vertical', 1, energy, border=0) testing.assert_allclose(out, 0) From 2e237887ba91be44e95d4b437324621e3f03df68 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sun, 29 Mar 2015 15:41:36 +0530 Subject: [PATCH 04/26] corrected title in example --- doc/examples/plot_seam_carving.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/examples/plot_seam_carving.py b/doc/examples/plot_seam_carving.py index 0583cd5d..c989f0c4 100644 --- a/doc/examples/plot_seam_carving.py +++ b/doc/examples/plot_seam_carving.py @@ -28,11 +28,11 @@ plt.title('Original Image') io.imshow(img, plugin='matplotlib') plt.figure() -plt.title('Resized Image Image') +plt.title('Resized Image') io.imshow(resized, plugin='matplotlib') plt.figure() -plt.title('Resized Image Image') +plt.title('Resized using Seam-Carving') io.imshow(out, plugin='matplotlib') io.show() From 222221c028f1333fe2bf9ccdd82d871451e4eb16 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sun, 29 Mar 2015 15:52:48 +0530 Subject: [PATCH 05/26] Added bento.info entry --- bento.info | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bento.info b/bento.info index cba403d9..9659df2a 100644 --- a/bento.info +++ b/bento.info @@ -161,6 +161,9 @@ Library: Extension: skimage.external.tifffile._tifffile Sources: skimage/external/tifffile/_tifffile.c + Extension: skimage.transform._seam_carving.pyx + Sources: + skimage/transform/_seam_carving.pyx Executable: skivi Module: skimage.scripts.skivi From 1a8ce172a117791c2f14aef83913d9de102836bc Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sun, 29 Mar 2015 15:55:30 +0530 Subject: [PATCH 06/26] Corrected bento entry --- bento.info | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bento.info b/bento.info index 9659df2a..7d21bfa8 100644 --- a/bento.info +++ b/bento.info @@ -161,7 +161,7 @@ Library: Extension: skimage.external.tifffile._tifffile Sources: skimage/external/tifffile/_tifffile.c - Extension: skimage.transform._seam_carving.pyx + Extension: skimage.transform._seam_carving Sources: skimage/transform/_seam_carving.pyx From 12fb4cbb860c992f035e235e28fec829b9e55726 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 30 Mar 2015 13:02:25 +0530 Subject: [PATCH 07/26] Removed debug print and corrected formatting --- skimage/transform/__init__.py | 4 ++-- skimage/transform/_seam_carving.pyx | 1 - skimage/transform/seam_carving.py | 2 +- skimage/transform/tests/test_seam_carving.py | 4 ++++ 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 05484887..9b40b3a9 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -12,7 +12,7 @@ from ._geometric import (warp, warp_coords, estimate_transform, from ._warps import swirl, resize, rotate, rescale, downscale_local_mean from .pyramids import (pyramid_reduce, pyramid_expand, pyramid_gaussian, pyramid_laplacian) -from seam_carving import seam_carve +from .seam_carving import seam_carve __all__ = ['hough_circle', @@ -45,4 +45,4 @@ __all__ = ['hough_circle', 'pyramid_expand', 'pyramid_gaussian', 'pyramid_laplacian', - 'seam_carve'] + 'seam_carve'] diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index 188c8dcb..86f3634a 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -78,7 +78,6 @@ cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, for row in range(rows-2, -1, -1): col = seam[row + 1] offset = track_img[row, col] - #print offset seam[row] = seam[row + 1] + offset return seam diff --git a/skimage/transform/seam_carving.py b/skimage/transform/seam_carving.py index 0777e377..beba4bb4 100644 --- a/skimage/transform/seam_carving.py +++ b/skimage/transform/seam_carving.py @@ -1,4 +1,4 @@ -from _seam_carving import _seam_carve_v +from ._seam_carving import _seam_carve_v from .. import util from .._shared import utils import numpy as np diff --git a/skimage/transform/tests/test_seam_carving.py b/skimage/transform/tests/test_seam_carving.py index d17eee25..40758648 100644 --- a/skimage/transform/tests/test_seam_carving.py +++ b/skimage/transform/tests/test_seam_carving.py @@ -32,3 +32,7 @@ def test_seam_carving(): out = transform.seam_carve(img3, 'vertical', 1, energy, border=0) testing.assert_allclose(out, 0) + + +if __name__ == '__main__': + np.testing.run_module_suite() From c332fd53637fbfb198c9c3e161a258c4e46aca9d Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 6 Jun 2015 21:46:50 +0530 Subject: [PATCH 08/26] docstring change --- skimage/transform/_seam_carving.pyx | 36 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index 86f3634a..b91887a5 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -13,7 +13,7 @@ cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, cnp.double_t[::1] current_cost, cnp.double_t[::1] prev_cost, Py_ssize_t cols): """Find a single vertical seam in an image that will be removed. - + Parameters ---------- energy_img : (M, N) ndarray @@ -32,19 +32,19 @@ cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, The number of cols to process for seam carving. Columns with indices more than `cols` are ignored. - + Returns ------- seam : (M, ) ndarray An array containing the index of the row of the pixel to be removed for each column in the image. - + Notes ----- `track_img`, `current_cost` and `prev_cost` are passed as arguments to avoid memory allocation at each iteration of `_seam_carve_v`. """ - + cdef Py_ssize_t rows, row, col rows = energy_img.shape[0] cdef cnp.double_t tmp, min_cost @@ -57,14 +57,14 @@ cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, for row in range(1, rows): for col in range(0, cols): - + min_cost = ABSOLUTE_MAX for offset in range(-1, 2): idx = col + offset - + if idx > cols - 1 or idx < 0: continue - + if prev_cost[idx] < min_cost: min_cost = prev_cost[idx] track_img[row, col] = offset @@ -85,27 +85,27 @@ cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, cdef remove_seam_v_2d(cnp.double_t[:, ::1] img, Py_ssize_t[::1] seam, Py_ssize_t cols): - cdef Py_ssize_t rows, row, col, idx - rows = img.shape[0] - """ Removes one horizontal seam from the image. + """ Removes one vertical seam from the image. The method modifies `img` so that all pixels to the right of the vertical seam are pushed one place left. image : (M, N) ndarray - Input image whose vertical seam is to be removed. + Input image whose vertical seam is to be removed. seam : (M, ) ndarray - An array use to store the index of the column in the seam for each row. + An array use to store the index of the column in the seam for each row. cols : int - Number of columns in the input image to process. Column indices more - than `cols` are ingored. + Number of columns in the input image to process. Column indices more + than `cols` are ingored. Notes ----- `seam` is passed as an argument so that we don't have to reallocate it for each iteration in `_seam_carve_v`. """ - + cdef Py_ssize_t rows, row, col, idx + rows = img.shape[0] + for row in range(rows): for idx in range(seam[row], cols - 1): img[row, idx] = img[row, idx + 1] @@ -171,7 +171,7 @@ def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): ------- image : (M, N - iters) or (M, N - iters, 3) ndarray The cropped image with the vertical seams removed. - + References ---------- .. [1] Shai Avidan and Ariel Shamir @@ -191,11 +191,11 @@ def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): sliced_img = img[:, 0:cols] energy_img = energy_func(sliced_img, *extra_args, **extra_kwargs) - + # So that borders are ignored. energy_img[:, 0:border] = ABSOLUTE_MAX energy_img[:, cols-border:cols] = ABSOLUTE_MAX - + seam = find_seam_v(energy_img, track_img, current_cost, prev_cost, cols) From cf98558196d5b0d57c413b82a0873462234689ce Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 6 Jun 2015 21:51:36 +0530 Subject: [PATCH 09/26] pep8 changes --- doc/examples/plot_seam_carving.py | 6 +++--- skimage/transform/_seam_carving.pyx | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/examples/plot_seam_carving.py b/doc/examples/plot_seam_carving.py index c989f0c4..6c31fbb8 100644 --- a/doc/examples/plot_seam_carving.py +++ b/doc/examples/plot_seam_carving.py @@ -15,13 +15,13 @@ using the Sobel filter to signify the importance of each pixel. """ from skimage import io, data from skimage import transform -from skimage import color, filters +from skimage import filters from matplotlib import pyplot as plt img = data.coins() -out = transform.seam_carve(img, 'vertical', 80, energy_func = filters.sobel) -out = transform.seam_carve(out, 'horizontal', 70, energy_func = filters.sobel) +out = transform.seam_carve(img, 'vertical', 80, energy_func=filters.sobel) +out = transform.seam_carve(out, 'horizontal', 70, energy_func=filters.sobel) resized = transform.resize(img, out.shape) plt.title('Original Image') diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index b91887a5..3b83e08f 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -6,7 +6,7 @@ import numpy as np cimport numpy as cnp -cdef cnp.double_t ABSOLUTE_MAX = np.finfo(np.double).max +cdef cnp.double_t DBL_MAX = np.finfo(np.double).max cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, @@ -22,10 +22,10 @@ cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, track_img : (M, N) ndarray The image used to store the optimal decision made at each point while finding a minimum cost path. - current_cost : (N, ) ndarray + current_cost : (N,) ndarray An array to store the current cost of the optimal path for each column in row currently being processed. - prev_cost : (N, ) ndarray + prev_cost : (N,) ndarray An array to store the current cost of the optimal path for each column in row prior to the one being processed. cols : int @@ -35,7 +35,7 @@ cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, Returns ------- - seam : (M, ) ndarray + seam : (M, ) ndarray of int An array containing the index of the row of the pixel to be removed for each column in the image. @@ -58,7 +58,7 @@ cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, for row in range(1, rows): for col in range(0, cols): - min_cost = ABSOLUTE_MAX + min_cost = DBL_MAX for offset in range(-1, 2): idx = col + offset From 2cc4066097bbf42ee965b876d4f1ae5a39adb04c Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 6 Jun 2015 22:42:18 +0530 Subject: [PATCH 10/26] 2d images converted to 3d before removing seams --- skimage/transform/_seam_carving.pyx | 50 ++++++----------------------- skimage/transform/seam_carving.py | 27 ++++++---------- 2 files changed, 20 insertions(+), 57 deletions(-) diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index 3b83e08f..3b64413c 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -9,7 +9,7 @@ cimport numpy as cnp cdef cnp.double_t DBL_MAX = np.finfo(np.double).max -cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, +cdef _find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, cnp.double_t[::1] current_cost, cnp.double_t[::1] prev_cost, Py_ssize_t cols): """Find a single vertical seam in an image that will be removed. @@ -18,10 +18,11 @@ cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, ---------- energy_img : (M, N) ndarray The energy image where a higher value signifies a pixel of more - importance. + importance. Pixels with a lower value will be cropped first. track_img : (M, N) ndarray The image used to store the optimal decision made at each point while - finding a minimum cost path. + finding a minimum cost path. For each pixel it stores the offset that + produced that least cost. current_cost : (N,) ndarray An array to store the current cost of the optimal path for each column in row currently being processed. @@ -83,35 +84,7 @@ cdef find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, return seam -cdef remove_seam_v_2d(cnp.double_t[:, ::1] img, Py_ssize_t[::1] seam, - Py_ssize_t cols): - """ Removes one vertical seam from the image. - - The method modifies `img` so that all pixels to the right of the vertical - seam are pushed one place left. - - image : (M, N) ndarray - Input image whose vertical seam is to be removed. - seam : (M, ) ndarray - An array use to store the index of the column in the seam for each row. - cols : int - Number of columns in the input image to process. Column indices more - than `cols` are ingored. - - Notes - ----- - `seam` is passed as an argument so that we don't have to reallocate it for - each iteration in `_seam_carve_v`. - """ - cdef Py_ssize_t rows, row, col, idx - rows = img.shape[0] - - for row in range(rows): - for idx in range(seam[row], cols - 1): - img[row, idx] = img[row, idx + 1] - - -cdef remove_seam_v_3d(cnp.double_t[:, :, ::1] img, Py_ssize_t[::1] seam, +cdef remove_seam_v(cnp.double_t[:, :, ::1] img, Py_ssize_t[::1] seam, Py_ssize_t cols): """ Removes one horizontal seam from the image. @@ -189,20 +162,17 @@ def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): for i in range(iters): - sliced_img = img[:, 0:cols] + sliced_img = np.squeeze(img[:, 0:cols]) energy_img = energy_func(sliced_img, *extra_args, **extra_kwargs) # So that borders are ignored. - energy_img[:, 0:border] = ABSOLUTE_MAX - energy_img[:, cols-border:cols] = ABSOLUTE_MAX + energy_img[:, 0:border] = DBL_MAX + energy_img[:, cols-border:cols] = DBL_MAX - seam = find_seam_v(energy_img, track_img, current_cost, prev_cost, + seam = _find_seam_v(energy_img, track_img, current_cost, prev_cost, cols) - if ndim == 2: - remove_seam_v_2d(img, seam, cols) - elif ndim == 3: - remove_seam_v_3d(img, seam, cols) + remove_seam_v(img, seam, cols) cols -= 1 diff --git a/skimage/transform/seam_carving.py b/skimage/transform/seam_carving.py index beba4bb4..28d92b74 100644 --- a/skimage/transform/seam_carving.py +++ b/skimage/transform/seam_carving.py @@ -55,23 +55,16 @@ def seam_carve(img, mode, num, energy_func, extra_args=[], """ utils.assert_nD(img, (2, 3)) - img = util.img_as_float(img) + image = util.img_as_float(img) - if mode == 'horizontal': - img = np.ascontiguousarray(img) - return _seam_carve_v(img, num, energy_func, extra_args, extra_kwargs, - border) - elif mode == 'vertical': - if img.ndim == 3: - img = np.transpose(img, (1, 0, 2)) - else: - img = img.T + if image.ndim == 2: + image = image[..., np.newaxis] - img = np.ascontiguousarray(img) - out = _seam_carve_v(img, num, energy_func, extra_args, extra_kwargs, - border) + if mode == 'vertical': + image = np.transpose(image, (1, 0, 2)) - if img.ndim == 3: - return np.transpose(out, (1, 0, 2)) - else: - return out.T + image = np.ascontiguousarray(image) + out = _seam_carve_v(image, num, energy_func, extra_args, extra_kwargs, border) + if mode == 'vertical': + out = np.transpose(out, (1, 0, 2)) + return np.squeeze(out) From 66caaa5b9fccd3daabff9fbcc42e50cfa0df41fa Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sun, 7 Jun 2015 00:40:05 +0530 Subject: [PATCH 11/26] Corrected testing code --- skimage/transform/tests/test_seam_carving.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/tests/test_seam_carving.py b/skimage/transform/tests/test_seam_carving.py index 40758648..57df75e0 100644 --- a/skimage/transform/tests/test_seam_carving.py +++ b/skimage/transform/tests/test_seam_carving.py @@ -5,7 +5,7 @@ from numpy import testing def energy(img): if(img.ndim == 3): - return np.ascontiguousarray(img[:, :, 0]) + img = np.ascontiguousarray(img[:, :, 0]) return (1 - img) From 37d8a8447b49f29f1782e4bc8e97f9712e26c36f Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 8 Jun 2015 23:11:57 +0530 Subject: [PATCH 12/26] Modifed code to use seam map --- doc/examples/plot_seam_carving.py | 14 ++-- skimage/transform/_seam_carving.pyx | 107 ++++++++++++++++++++++------ skimage/transform/seam_carving.py | 4 +- 3 files changed, 94 insertions(+), 31 deletions(-) diff --git a/doc/examples/plot_seam_carving.py b/doc/examples/plot_seam_carving.py index 6c31fbb8..a994fef2 100644 --- a/doc/examples/plot_seam_carving.py +++ b/doc/examples/plot_seam_carving.py @@ -18,21 +18,23 @@ from skimage import transform from skimage import filters from matplotlib import pyplot as plt +def nothing(img): + return img -img = data.coins() +img = data.coins()#io.imread('/home/vighnesh/images/seam_bw.png') out = transform.seam_carve(img, 'vertical', 80, energy_func=filters.sobel) -out = transform.seam_carve(out, 'horizontal', 70, energy_func=filters.sobel) +#out = transform.seam_carve(out, 'horizontal', 70, energy_func=filters.sobel) resized = transform.resize(img, out.shape) plt.title('Original Image') -io.imshow(img, plugin='matplotlib') +#io.imshow(img, plugin='matplotlib') plt.figure() plt.title('Resized Image') -io.imshow(resized, plugin='matplotlib') +#io.imshow(resized, plugin='matplotlib') plt.figure() plt.title('Resized using Seam-Carving') -io.imshow(out, plugin='matplotlib') +#io.imshow(out, plugin='matplotlib') -io.show() +#io.show() diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index 3b64413c..3be4c54f 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -1,7 +1,3 @@ -# cython: cdivision=True -# cython: boundscheck=False -# cython: nonecheck=False -# cython: wraparound=False import numpy as np cimport numpy as cnp @@ -112,6 +108,65 @@ cdef remove_seam_v(cnp.double_t[:, :, ::1] img, Py_ssize_t[::1] seam, img[row, idx, :] = img[row, idx + 1, :] +cdef _preprocess_image(cnp.double_t[:, ::1] energy_img, + cnp.double_t[:, ::1] cumulative_img, + cnp.int8_t[:, ::1] track_img, + Py_ssize_t cols): + + cdef Py_ssize_t r, c, offset, c_idx + cdef Py_ssize_t rows = energy_img.shape[0] + cdef cnp.double_t min_cost = DBL_MAX + + for c in range(cols): + cumulative_img[0, c] = energy_img[0, c] + + + for r in range(1, rows): + for c in range(cols): + min_cost = DBL_MAX + for offset in range(-1, 2): + + c_idx = c + offset + if (c_idx > cols - 1) or (c_idx < 0) : + continue + + if cumulative_img[r-1, c_idx] < min_cost: + min_cost = cumulative_img[r-1, c_idx] + track_img[r, c] = offset + + #print "min_cost = ", min_cost + cumulative_img[r,c] = min_cost + energy_img[r, c] + + #print "-------Cumulative Image --------" + #print np.array(cumulative_img) + #print "-------Energy Image --------" + #print np.array(energy_img) + +cdef cnp.uint8_t mark_seam(cnp.int8_t[:, ::1] track_img, Py_ssize_t start_index, + cnp.uint8_t[:, ::1] seam_map): + + cdef Py_ssize_t rows = track_img.shape[0] + cdef Py_ssize_t[::1] current_seam_indices = np.zeros(rows, dtype=np.int) + cdef Py_ssize_t row, col + cdef cnp.int8_t offset + cdef Py_ssize_t seams + + current_seam_indices[rows - 1] = start_index + for row in range(rows - 2, -1, -1): + col = current_seam_indices[row+1] + offset = track_img[row, col] + col = col + offset + current_seam_indices[row] = col + + if seam_map[row, col]: + return 0 + + + for row in range(rows): + col = current_seam_indices[row] + seam_map[row, col] = 1 + + return 1 def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): """ Carve vertical seams off an image. @@ -120,7 +175,7 @@ def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): Parameters ---------- - image : (M, N) or (M, N, 3) ndarray + img : (M, N) or (M, N, 3) ndarray Input image whose vertical seams are to be removed. iters : int Number of vertical seams are to be removed. @@ -151,29 +206,35 @@ def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): "Seam Carving for Content-Aware Image Resizing" http://www.cs.jhu.edu/~misha/ReadingSeminar/Papers/Avidan07.pdf """ - cdef Py_ssize_t[::1] seam - cdef Py_ssize_t ndim = img.ndim + last_row_obj = np.zeros(img.shape[1], dtype=np.float) + seam_map_obj = np.zeros(img.shape[0:2], dtype=np.uint8) + + cdef cnp.double_t[::1] last_row = last_row_obj + cdef Py_ssize_t[::1] sorted_indices + cdef cnp.uint8_t[:, ::1] seam_map = seam_map_obj cdef Py_ssize_t cols = img.shape[1] - track_img = np.zeros(img.shape[0:2], dtype=np.int8) + cdef cnp.double_t[:, :, ::1] image = img + cdef cnp.int8_t[:, ::1] track_img = np.zeros(img.shape[0:2], dtype=np.int8) + cdef cnp.double_t[:, ::1] cumulative_img = np.zeros(img.shape[0:2], dtype=np.float) + cdef cnp.double_t[:, ::1] energy_img - current_cost = np.zeros_like(track_img[0], dtype = img.dtype) - prev_cost = np.zeros_like(track_img[0], dtype = img.dtype) + energy_img_obj = energy_func(np.squeeze(img)) + energy_img = energy_img_obj - for i in range(iters): + energy_img_obj[:, 0:border] = DBL_MAX + energy_img_obj[:, cols-border:cols] = DBL_MAX - sliced_img = np.squeeze(img[:, 0:cols]) - energy_img = energy_func(sliced_img, *extra_args, **extra_kwargs) + _preprocess_image(energy_img, cumulative_img, track_img, cols) + last_row[...] = cumulative_img[-1, :] + sorted_indices = np.argsort(last_row_obj) + #print "Sorted Indices = ", np.array(sorted_indices) + #print "First sorted Index = ", sorted_indices[0] + #print "Last Row = ", np.array(energy_img[-1, :]) + #print np.array() - # So that borders are ignored. - energy_img[:, 0:border] = DBL_MAX - energy_img[:, cols-border:cols] = DBL_MAX - - seam = _find_seam_v(energy_img, track_img, current_cost, prev_cost, - cols) - - remove_seam_v(img, seam, cols) - - cols -= 1 + from skimage import io + io.imshow(seam_map_obj*255) + io.show() return img[:, 0:cols] diff --git a/skimage/transform/seam_carving.py b/skimage/transform/seam_carving.py index 28d92b74..af329036 100644 --- a/skimage/transform/seam_carving.py +++ b/skimage/transform/seam_carving.py @@ -60,11 +60,11 @@ def seam_carve(img, mode, num, energy_func, extra_args=[], if image.ndim == 2: image = image[..., np.newaxis] - if mode == 'vertical': + if mode == 'horizontal': image = np.transpose(image, (1, 0, 2)) image = np.ascontiguousarray(image) out = _seam_carve_v(image, num, energy_func, extra_args, extra_kwargs, border) - if mode == 'vertical': + if mode == 'horizontal': out = np.transpose(out, (1, 0, 2)) return np.squeeze(out) From 49037f69b0041ddc3bd512c9b6697e5c9adb5340 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 10 Jun 2015 00:11:03 +0530 Subject: [PATCH 13/26] added removal function --- doc/examples/plot_seam_carving.py | 18 +-- skimage/transform/_seam_carving.pyx | 175 +++++++++------------------- 2 files changed, 65 insertions(+), 128 deletions(-) diff --git a/doc/examples/plot_seam_carving.py b/doc/examples/plot_seam_carving.py index a994fef2..bd935a31 100644 --- a/doc/examples/plot_seam_carving.py +++ b/doc/examples/plot_seam_carving.py @@ -15,26 +15,28 @@ using the Sobel filter to signify the importance of each pixel. """ from skimage import io, data from skimage import transform -from skimage import filters +from skimage import filters, color from matplotlib import pyplot as plt def nothing(img): return img -img = data.coins()#io.imread('/home/vighnesh/images/seam_bw.png') -out = transform.seam_carve(img, 'vertical', 80, energy_func=filters.sobel) +#img = io.imread('/home/vighnesh/images/castle.jpg') +#img = color.rgb2gray(img) +img = data.camera() +out = transform.seam_carve(img, 'vertical', 50, energy_func=filters.sobel) #out = transform.seam_carve(out, 'horizontal', 70, energy_func=filters.sobel) resized = transform.resize(img, out.shape) plt.title('Original Image') -#io.imshow(img, plugin='matplotlib') +io.imshow(img, plugin='matplotlib') -plt.figure() -plt.title('Resized Image') +#plt.figure() +#plt.title('Resized Image') #io.imshow(resized, plugin='matplotlib') plt.figure() plt.title('Resized using Seam-Carving') -#io.imshow(out, plugin='matplotlib') +io.imshow(out, plugin='matplotlib') -#io.show() +io.show() diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index 3be4c54f..d63374d4 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -4,111 +4,7 @@ cimport numpy as cnp cdef cnp.double_t DBL_MAX = np.finfo(np.double).max - -cdef _find_seam_v(cnp.double_t[:, ::1] energy_img, cnp.int8_t[:, ::1] track_img, - cnp.double_t[::1] current_cost, cnp.double_t[::1] prev_cost, - Py_ssize_t cols): - """Find a single vertical seam in an image that will be removed. - - Parameters - ---------- - energy_img : (M, N) ndarray - The energy image where a higher value signifies a pixel of more - importance. Pixels with a lower value will be cropped first. - track_img : (M, N) ndarray - The image used to store the optimal decision made at each point while - finding a minimum cost path. For each pixel it stores the offset that - produced that least cost. - current_cost : (N,) ndarray - An array to store the current cost of the optimal path for each column - in row currently being processed. - prev_cost : (N,) ndarray - An array to store the current cost of the optimal path for each column - in row prior to the one being processed. - cols : int - The number of cols to process for seam carving. Columns with indices - more than `cols` are ignored. - - - Returns - ------- - seam : (M, ) ndarray of int - An array containing the index of the row of the pixel to be removed - for each column in the image. - - Notes - ----- - `track_img`, `current_cost` and `prev_cost` are passed as arguments to - avoid memory allocation at each iteration of `_seam_carve_v`. - """ - - cdef Py_ssize_t rows, row, col - rows = energy_img.shape[0] - cdef cnp.double_t tmp, min_cost - cdef Py_ssize_t offset, idx, offset_clip - - cdef Py_ssize_t[::1] seam = np.zeros(rows, dtype=np.int) - - for idx in range(cols): - prev_cost[idx] = energy_img[0, idx] - - for row in range(1, rows): - for col in range(0, cols): - - min_cost = DBL_MAX - for offset in range(-1, 2): - idx = col + offset - - if idx > cols - 1 or idx < 0: - continue - - if prev_cost[idx] < min_cost: - min_cost = prev_cost[idx] - track_img[row, col] = offset - - current_cost[col] = min_cost + energy_img[row, col] - - prev_cost[:] = current_cost - - seam[rows-1] = np.argmin(current_cost) - - for row in range(rows-2, -1, -1): - col = seam[row + 1] - offset = track_img[row, col] - seam[row] = seam[row + 1] + offset - - return seam - - -cdef remove_seam_v(cnp.double_t[:, :, ::1] img, Py_ssize_t[::1] seam, - Py_ssize_t cols): - """ Removes one horizontal seam from the image. - - The method modifies `img` so that all pixels to the right of the vertical - seam are pushed one place left. - - image : (M, N, 3) ndarray - Input image whose vertical seam is to be removed. - seam : (M, ) ndarray - An array use to store the index of the column in the seam for each row. - cols : int - Number of columns in the input image to process. Column indices more - than `cols` are ingored. - - Notes - ----- - `seam` is passed as an argument so that we don't have to reallocate it for - each iteration in `_seam_carve_v`. - """ - cdef Py_ssize_t rows, row, col, idx - rows = img.shape[0] - - for row in range(rows): - for idx in range(seam[row], cols - 1): - img[row, idx, :] = img[row, idx + 1, :] - - -cdef _preprocess_image(cnp.double_t[:, ::1] energy_img, +cdef _preprocess_image(cnp.double_t[:, :, ::1] energy_img, cnp.double_t[:, ::1] cumulative_img, cnp.int8_t[:, ::1] track_img, Py_ssize_t cols): @@ -118,7 +14,7 @@ cdef _preprocess_image(cnp.double_t[:, ::1] energy_img, cdef cnp.double_t min_cost = DBL_MAX for c in range(cols): - cumulative_img[0, c] = energy_img[0, c] + cumulative_img[0, c] = energy_img[0, c, 0] for r in range(1, rows): @@ -135,7 +31,7 @@ cdef _preprocess_image(cnp.double_t[:, ::1] energy_img, track_img[r, c] = offset #print "min_cost = ", min_cost - cumulative_img[r,c] = min_cost + energy_img[r, c] + cumulative_img[r,c] = min_cost + energy_img[r, c, 0] #print "-------Cumulative Image --------" #print np.array(cumulative_img) @@ -159,14 +55,29 @@ cdef cnp.uint8_t mark_seam(cnp.int8_t[:, ::1] track_img, Py_ssize_t start_index, current_seam_indices[row] = col if seam_map[row, col]: + #print "---------- Seam conflict at ", row, col return 0 - for row in range(rows): col = current_seam_indices[row] seam_map[row, col] = 1 return 1 + +cdef remove_seam(cnp.double_t[:, :, ::1] img, + cnp.uint8_t[:, ::1] seam_map, Py_ssize_t cols): + + cdef Py_ssize_t rows = img.shape[0] + cdef Py_ssize_t channels = img.shape[2] + cdef Py_ssize_t r, c, ch, shift + + for r in range(rows): + shift = 0 + for c in range(cols): + shift += seam_map[r, c] + for ch in range(channels): + img[r, c, ch] = img[r, c + shift, ch] + def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): """ Carve vertical seams off an image. @@ -213,28 +124,52 @@ def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): cdef Py_ssize_t[::1] sorted_indices cdef cnp.uint8_t[:, ::1] seam_map = seam_map_obj cdef Py_ssize_t cols = img.shape[1] + cdef Py_ssize_t rows = img.shape[0] + cdef Py_ssize_t seams_left = iters + cdef Py_ssize_t seams_removed + cdef Py_ssize_t seam_idx cdef cnp.double_t[:, :, ::1] image = img cdef cnp.int8_t[:, ::1] track_img = np.zeros(img.shape[0:2], dtype=np.int8) cdef cnp.double_t[:, ::1] cumulative_img = np.zeros(img.shape[0:2], dtype=np.float) - cdef cnp.double_t[:, ::1] energy_img + cdef cnp.double_t[:, :, ::1] energy_img - energy_img_obj = energy_func(np.squeeze(img)) + energy_img_obj = energy_func(np.squeeze(img))[:, :, np.newaxis]**2 + energy_img_obj = np.ascontiguousarray(energy_img_obj) energy_img = energy_img_obj - energy_img_obj[:, 0:border] = DBL_MAX - energy_img_obj[:, cols-border:cols] = DBL_MAX + energy_img_obj[:, 0:border, 0] = DBL_MAX + energy_img_obj[:, cols-border:cols, 0] = DBL_MAX + energy_img_obj[rows-border:rows,:,0] = energy_img_obj[rows-2*border:rows-border,:,0] + _preprocess_image(energy_img, cumulative_img, track_img, cols) last_row[...] = cumulative_img[-1, :] sorted_indices = np.argsort(last_row_obj) - #print "Sorted Indices = ", np.array(sorted_indices) - #print "First sorted Index = ", sorted_indices[0] - #print "Last Row = ", np.array(energy_img[-1, :]) - #print np.array() + seam_idx = 0 - from skimage import io - io.imshow(seam_map_obj*255) - io.show() - return img[:, 0:cols] + while seams_left > 0: + #print "sorted indices", np.array(sorted_indices)[:10] + #print "sorted array ", np.sort(last_row_obj)[:10] + #print "Seam starting at : ", sorted_indices[seam_idx] + if mark_seam(track_img, sorted_indices[seam_idx], seam_map): + seams_left -= 1 + cols -= 1 + #print "Seam marked ", seam_idx + seam_idx += 1 + continue + else: + print "Seams removed = ", seam_idx + seam_idx = 0 + remove_seam(image, seam_map, cols) + remove_seam(energy_img, seam_map, cols) + seam_map[...] = 0 + _preprocess_image(energy_img, cumulative_img, track_img, cols) + last_row[:cols] = cumulative_img[-1, :cols] + sorted_indices = np.argsort(last_row_obj) + + #from skimage import io + #io.imshow(seam_map_obj*255) + #io.show() + return img#[:, 0:cols] From eac5663251f599791c3db9475f62b5f516adf459 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 10 Jun 2015 21:39:09 +0530 Subject: [PATCH 14/26] Changed signatures to use energy map --- doc/examples/plot_seam_carving.py | 7 ++-- skimage/transform/_seam_carving.pyx | 56 +++++++++++------------------ skimage/transform/seam_carving.py | 43 ++++++++++------------ 3 files changed, 43 insertions(+), 63 deletions(-) diff --git a/doc/examples/plot_seam_carving.py b/doc/examples/plot_seam_carving.py index bd935a31..67da1395 100644 --- a/doc/examples/plot_seam_carving.py +++ b/doc/examples/plot_seam_carving.py @@ -21,10 +21,11 @@ from matplotlib import pyplot as plt def nothing(img): return img -#img = io.imread('/home/vighnesh/images/castle.jpg') +img = io.imread('/home/vighnesh/images/rocket.jpg') #img = color.rgb2gray(img) -img = data.camera() -out = transform.seam_carve(img, 'vertical', 50, energy_func=filters.sobel) +eimg = filters.sobel(color.rgb2gray(img)) +#img = data.camera() +out = transform.seam_carve(img, eimg, 'vertical', 200) #out = transform.seam_carve(out, 'horizontal', 70, energy_func=filters.sobel) resized = transform.resize(img, out.shape) diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index d63374d4..7dfcc8c4 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -78,11 +78,11 @@ cdef remove_seam(cnp.double_t[:, :, ::1] img, for ch in range(channels): img[r, c, ch] = img[r, c + shift, ch] -def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): +def _seam_carve_v(img, energy_map, iters, border): """ Carve vertical seams off an image. Carves out vertical seams off an image while using the given energy - function to decide the importance of each pixel.[1] + map to decide the importance of each pixel.[1] Parameters ---------- @@ -90,25 +90,21 @@ def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): Input image whose vertical seams are to be removed. iters : int Number of vertical seams are to be removed. - energy_func : callable - The function used to decide the importance of each pixel. The higher + energy_map : (M, N) ndarray + The array to decide the importance of each pixel. The higher the value corresponding to a pixel, the more the algorithm will try - to keep it in the image. For every iteration `energy_func` is called - as `energy_func(image, *extra_args, **extra_kwargs)`, where `image` - is the cropped image during each iteration and is expected to return a - (M, N) ndarray depicting each pixel's importance. - extra_args : iterable - The extra arguments supplied to `energy_func`. - extra_kwargs : dict - The extra keyword arguments supplied to `energy_func`. - border : int - The number of pixels in the right and left end of the image to be - excluded from being considered for a seam. This is important as certain - filters just ignore image boundaries and set them to `0`. + to keep it in the image. + num : int + Number of seams are to be removed. + border : int, optional + The number of pixels in the right, left and bottom end of the image + to be excluded from being considered for a seam. This is important as + certain filters just ignore image boundaries and set them to `0`. + By default border is set to `1`. Returns ------- - image : (M, N - iters) or (M, N - iters, 3) ndarray + image : (M, N - iters, 3) ndarray of float The cropped image with the vertical seams removed. References @@ -118,11 +114,10 @@ def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): http://www.cs.jhu.edu/~misha/ReadingSeminar/Papers/Avidan07.pdf """ last_row_obj = np.zeros(img.shape[1], dtype=np.float) - seam_map_obj = np.zeros(img.shape[0:2], dtype=np.uint8) cdef cnp.double_t[::1] last_row = last_row_obj cdef Py_ssize_t[::1] sorted_indices - cdef cnp.uint8_t[:, ::1] seam_map = seam_map_obj + cdef cnp.uint8_t[:, ::1] seam_map = np.zeros(img.shape[0:2], dtype=np.uint8) cdef Py_ssize_t cols = img.shape[1] cdef Py_ssize_t rows = img.shape[0] cdef Py_ssize_t seams_left = iters @@ -134,33 +129,25 @@ def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): cdef cnp.double_t[:, ::1] cumulative_img = np.zeros(img.shape[0:2], dtype=np.float) cdef cnp.double_t[:, :, ::1] energy_img - energy_img_obj = energy_func(np.squeeze(img))[:, :, np.newaxis]**2 - energy_img_obj = np.ascontiguousarray(energy_img_obj) - energy_img = energy_img_obj - - energy_img_obj[:, 0:border, 0] = DBL_MAX - energy_img_obj[:, cols-border:cols, 0] = DBL_MAX - energy_img_obj[rows-border:rows,:,0] = energy_img_obj[rows-2*border:rows-border,:,0] + energy_map[:, 0:border] = DBL_MAX + energy_map[:, cols-border:cols] = DBL_MAX + energy_map[rows-border:rows, :] = energy_map[rows-2*border:rows-border, :] + energy_map = np.ascontiguousarray(energy_map[:, :, np.newaxis]) + energy_img = energy_map _preprocess_image(energy_img, cumulative_img, track_img, cols) last_row[...] = cumulative_img[-1, :] sorted_indices = np.argsort(last_row_obj) seam_idx = 0 - while seams_left > 0: - #print "sorted indices", np.array(sorted_indices)[:10] - #print "sorted array ", np.sort(last_row_obj)[:10] - #print "Seam starting at : ", sorted_indices[seam_idx] if mark_seam(track_img, sorted_indices[seam_idx], seam_map): seams_left -= 1 cols -= 1 - #print "Seam marked ", seam_idx seam_idx += 1 continue else: - print "Seams removed = ", seam_idx seam_idx = 0 remove_seam(image, seam_map, cols) remove_seam(energy_img, seam_map, cols) @@ -169,7 +156,4 @@ def _seam_carve_v(img, iters, energy_func, extra_args , extra_kwargs, border): last_row[:cols] = cumulative_img[-1, :cols] sorted_indices = np.argsort(last_row_obj) - #from skimage import io - #io.imshow(seam_map_obj*255) - #io.show() - return img#[:, 0:cols] + return img[:, 0:cols] diff --git a/skimage/transform/seam_carving.py b/skimage/transform/seam_carving.py index af329036..3caba0de 100644 --- a/skimage/transform/seam_carving.py +++ b/skimage/transform/seam_carving.py @@ -4,43 +4,36 @@ from .._shared import utils import numpy as np -def seam_carve(img, mode, num, energy_func, extra_args=[], - extra_kwargs={}, border=1, force_copy=True): +def seam_carve(img, energy_map, mode, num, border=1, force_copy=True): """ Carve vertical or horizontal seams off an image. Carves out vertical/horizontal seams off an image while using the given - energy function to decide the importance of each pixel. + energy map to decide the importance of each pixel. Parameters ---------- image : (M, N) or (M, N, 3) ndarray - Input image whose vertical seams are to be removed. + Input image whose seams are to be removed. + energy_map : (M, N) ndarray + The array to decide the importance of each pixel. The higher + the value corresponding to a pixel, the more the algorithm will try + to keep it in the image. mode : str {'horizontal', 'vertical'} Indicates whether seams are to be removed vertically or horizontally. Removing seams horizontally will decrease the height whereas removing vertically will decrease the width. num : int Number of seams are to be removed. - energy_func : callable - The function used to decide the importance of each pixel. The higher - the value corresponding to a pixel, the more the algorithm will try - to keep it in the image. For every iteration `energy_func` is called - as `energy_func(image, *extra_args, **extra_kwargs)`, where `image` - is the cropped image during each iteration and is expected to return a - (M, N) ndarray depicting each pixel's importance. - extra_args : iterable, optional - The extra arguments supplied to `energy_func`. - extra_kwargs : dict, optional - The extra keyword arguments supplied to `energy_func`. border : int, optional - The number of pixels in the right and left end of the image to be - excluded from being considered for a seam. This is important as certain - filters just ignore image boundaries and set them to `0`. By default - border is set to `1`. + The number of pixels in the right, left and bottom end of the image + to be excluded from being considered for a seam. This is important as + certain filters just ignore image boundaries and set them to `0`. + By default border is set to `1`. force_copy : bool, optional - If set, the image is copied before being used by the method which - modifies it in place. Set this to `False` if the original image is no - loner needed after this opetration. + If set, the `image` and `energy_map` are copied before being used by + the method which modifies it in place. Set this to `False` if the + original image and the energy map are no longer needed after + this opetration. Returns ------- @@ -55,7 +48,8 @@ def seam_carve(img, mode, num, energy_func, extra_args=[], """ utils.assert_nD(img, (2, 3)) - image = util.img_as_float(img) + image = util.img_as_float(img, force_copy) + energy_map = util.img_as_float(energy_map, force_copy) if image.ndim == 2: image = image[..., np.newaxis] @@ -64,7 +58,8 @@ def seam_carve(img, mode, num, energy_func, extra_args=[], image = np.transpose(image, (1, 0, 2)) image = np.ascontiguousarray(image) - out = _seam_carve_v(image, num, energy_func, extra_args, extra_kwargs, border) + out = _seam_carve_v(image, energy_map, num, border) + if mode == 'horizontal': out = np.transpose(out, (1, 0, 2)) return np.squeeze(out) From ae35dcceb4372ecefd9439a073dbb8ea09f90d50 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 10 Jun 2015 22:28:09 +0530 Subject: [PATCH 15/26] docstring updates --- skimage/transform/_seam_carving.pyx | 69 +++++++++++++++++++++++------ skimage/transform/seam_carving.py | 1 + 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index 7dfcc8c4..c2ad8bfa 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -8,6 +8,25 @@ cdef _preprocess_image(cnp.double_t[:, :, ::1] energy_img, cnp.double_t[:, ::1] cumulative_img, cnp.int8_t[:, ::1] track_img, Py_ssize_t cols): + """ For each row, compute the lowest seam value for all its columns. + + This function updates `cumulative_img` such that `cumulative_img[r, c]` + is the total energy of the lowest energy seam ending at `(r, c)`. + + Parameters + ---------- + energy_img : (M, N, 1) ndarray + The array of cost of removal of each pixel. Seam carving tries to avoid + pixels with high costs. + cumulative_img : (M, N) ndarray + The array to be updated with the total cost of lowest energy seams. + track_img : (M, N) ndarray + For each pixel, `track_img` stores the relative column offset in + the previous row which has the lowest value in `cumulative_img`. This + helps in in re-tracing the minimum cost seam. + cols : int + Number of columns to process. + """ cdef Py_ssize_t r, c, offset, c_idx cdef Py_ssize_t rows = energy_img.shape[0] @@ -16,7 +35,6 @@ cdef _preprocess_image(cnp.double_t[:, :, ::1] energy_img, for c in range(cols): cumulative_img[0, c] = energy_img[0, c, 0] - for r in range(1, rows): for c in range(cols): min_cost = DBL_MAX @@ -30,17 +48,33 @@ cdef _preprocess_image(cnp.double_t[:, :, ::1] energy_img, min_cost = cumulative_img[r-1, c_idx] track_img[r, c] = offset - #print "min_cost = ", min_cost cumulative_img[r,c] = min_cost + energy_img[r, c, 0] - #print "-------Cumulative Image --------" - #print np.array(cumulative_img) - #print "-------Energy Image --------" - #print np.array(energy_img) +cdef cnp.uint8_t _mark_seam(cnp.int8_t[:, ::1] track_img, + Py_ssize_t start_index, + cnp.uint8_t[:, ::1] seam_map): -cdef cnp.uint8_t mark_seam(cnp.int8_t[:, ::1] track_img, Py_ssize_t start_index, - cnp.uint8_t[:, ::1] seam_map): + """ Re-trace the optimal seam from a given column in the last row. + This function tries to re-track an optimal seam from `start_index` and + tries to mark it in `seam_map`. If this seam intersects with any existing + seam in `seam_map` the function returns `0` without marking anything. Else + it marks the seam in `seam_map` and returns `1`. + + track_img : (M, N) ndarray + The array of relative column indices as updated by `_preprocess_image`. + start_index : int + The column number of the bottom most row from where to start re-tracing + the seam. + seam_map : (M, N) ndarray + The array used to mark seams. If a pixel is marked as as seam it is set + to `1`, else `0`. + + Returns + ------- + out : int + `1` if seam was marked, `0` is seam inersects and was not marked. + """ cdef Py_ssize_t rows = track_img.shape[0] cdef Py_ssize_t[::1] current_seam_indices = np.zeros(rows, dtype=np.int) cdef Py_ssize_t row, col @@ -55,7 +89,6 @@ cdef cnp.uint8_t mark_seam(cnp.int8_t[:, ::1] track_img, Py_ssize_t start_index, current_seam_indices[row] = col if seam_map[row, col]: - #print "---------- Seam conflict at ", row, col return 0 for row in range(rows): @@ -64,9 +97,19 @@ cdef cnp.uint8_t mark_seam(cnp.int8_t[:, ::1] track_img, Py_ssize_t start_index, return 1 -cdef remove_seam(cnp.double_t[:, :, ::1] img, +cdef _remove_seam(cnp.double_t[:, :, ::1] img, cnp.uint8_t[:, ::1] seam_map, Py_ssize_t cols): + """ Removes marked seams from an image. + Parameters + ---------- + img : (M, N, P) ndarray + Input image whose vertical seams are to be removed. + seam_map : (M, N) ndarray + Array with seams to be removed marked by non-zero entries. + cols : int + The number of colums to process. + """ cdef Py_ssize_t rows = img.shape[0] cdef Py_ssize_t channels = img.shape[2] cdef Py_ssize_t r, c, ch, shift @@ -142,15 +185,15 @@ def _seam_carve_v(img, energy_map, iters, border): seam_idx = 0 while seams_left > 0: - if mark_seam(track_img, sorted_indices[seam_idx], seam_map): + if _mark_seam(track_img, sorted_indices[seam_idx], seam_map): seams_left -= 1 cols -= 1 seam_idx += 1 continue else: seam_idx = 0 - remove_seam(image, seam_map, cols) - remove_seam(energy_img, seam_map, cols) + _remove_seam(image, seam_map, cols) + _remove_seam(energy_img, seam_map, cols) seam_map[...] = 0 _preprocess_image(energy_img, cumulative_img, track_img, cols) last_row[:cols] = cumulative_img[-1, :cols] diff --git a/skimage/transform/seam_carving.py b/skimage/transform/seam_carving.py index 3caba0de..e037b6e1 100644 --- a/skimage/transform/seam_carving.py +++ b/skimage/transform/seam_carving.py @@ -62,4 +62,5 @@ def seam_carve(img, energy_map, mode, num, border=1, force_copy=True): if mode == 'horizontal': out = np.transpose(out, (1, 0, 2)) + return np.squeeze(out) From 1e49f498465afbff28a15655c67afcf9e261f648 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 11 Jun 2015 00:14:39 +0530 Subject: [PATCH 16/26] Added rocket image --- doc/examples/plot_seam_carving.py | 82 ++++++++++++++++++++++++------ skimage/data/__init__.py | 19 +++++++ skimage/data/rocket.jpg | Bin 0 -> 112525 bytes 3 files changed, 85 insertions(+), 16 deletions(-) create mode 100644 skimage/data/rocket.jpg diff --git a/doc/examples/plot_seam_carving.py b/doc/examples/plot_seam_carving.py index 67da1395..9ed7181c 100644 --- a/doc/examples/plot_seam_carving.py +++ b/doc/examples/plot_seam_carving.py @@ -13,31 +13,81 @@ using the Sobel filter to signify the importance of each pixel. http://www.cs.jhu.edu/~misha/ReadingSeminar/Papers/Avidan07.pdf """ -from skimage import io, data -from skimage import transform +from skimage import data, draw +from skimage import transform, util +import numpy as np from skimage import filters, color from matplotlib import pyplot as plt -def nothing(img): - return img -img = io.imread('/home/vighnesh/images/rocket.jpg') -#img = color.rgb2gray(img) +hl_color = np.array([0, 1, 0]) + +img = data.rocket() +img = util.img_as_float(img) eimg = filters.sobel(color.rgb2gray(img)) -#img = data.camera() -out = transform.seam_carve(img, eimg, 'vertical', 200) -#out = transform.seam_carve(out, 'horizontal', 70, energy_func=filters.sobel) -resized = transform.resize(img, out.shape) plt.title('Original Image') -io.imshow(img, plugin='matplotlib') +plt.imshow(img) -#plt.figure() -#plt.title('Resized Image') -#io.imshow(resized, plugin='matplotlib') +""" +.. image:: PLOT2RST.current_figure +""" +resized = transform.resize(img, (img.shape[0], img.shape[1] - 200)) +plt.figure() +plt.title('Resized Image') +plt.imshow(resized) + + +""" +.. image:: PLOT2RST.current_figure +""" + +out = transform.seam_carve(img, eimg, 'vertical', 200) plt.figure() plt.title('Resized using Seam-Carving') -io.imshow(out, plugin='matplotlib') +plt.imshow(out) -io.show() +""" +.. image:: PLOT2RST.current_figure + +As you can see, resizing as distorted the rocket and the objects around, +whereas seam carving has reszied by removing the empty spaces in between. + +Object Removal +-------------- + +Seam Carving can also be used to remove atrifacts from images. To do that, we +have to ensure that pixels to be removes get less importance. In the following +code I approximately mark the rocket with a mask, and then decrease the +importance of those pixels + +""" + +masked_img = img.copy() + +poly = [(404, 281), (404, 360), (359, 364), (338, 337), (145, 337), (120, 322), (145, 304), (340, 306), (362, 284)] +pr = np.array([p[0] for p in poly]) +pc = np.array([p[1] for p in poly]) +rr, cc = draw.polygon(pr, pc) + +masked_img[rr,cc,:] = masked_img[rr,cc,:]*0.5 + hl_color*.5 +plt.figure() +plt.title('Object Marked') + +plt.imshow(masked_img) +""" +.. image:: PLOT2RST.current_figure +""" + +eimg[rr, cc] -= 1000 + +plt.figure() +plt.title('Object Removed') +out = transform.seam_carve(img, eimg, 'vertical', 90) +resized = transform.resize(img, out.shape) +plt.imshow(out) +plt.show() +""" +.. image:: PLOT2RST.current_figure +""" diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index 2be829ac..98fb4e5f 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -26,6 +26,7 @@ __all__ = ['load', 'chelsea', 'coffee', 'hubble_deep_field', + 'rocket', 'astronaut'] @@ -241,3 +242,21 @@ def hubble_deep_field(): """ return load("hubble_deep_field.jpg") + +def rocket(): + """Launch photo of DSCOVR on Falcon 9 by SpaceX. + + This is the launch photo of Falcon 9 carrying DSCOVR lifted off from + SpaceX's Launch Complex 40 at Cape Canaveral Air Force Station, Fla.. + + Notes + ----- + This image was downloaded from + `SpaceX Photos + `__. + + The image was captured by SpaceX and `released in the public domain + `_. + + """ + return load("rocket.jpg") diff --git a/skimage/data/rocket.jpg b/skimage/data/rocket.jpg new file mode 100644 index 0000000000000000000000000000000000000000..32bdd33960c60d012fc71b5fd80a7ccc24d3b906 GIT binary patch literal 112525 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 Date: Thu, 11 Jun 2015 00:20:09 +0530 Subject: [PATCH 17/26] Formatting changes --- doc/examples/plot_seam_carving.py | 5 +++-- skimage/transform/_seam_carving.pyx | 13 ++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/doc/examples/plot_seam_carving.py b/doc/examples/plot_seam_carving.py index 9ed7181c..c5250d32 100644 --- a/doc/examples/plot_seam_carving.py +++ b/doc/examples/plot_seam_carving.py @@ -66,12 +66,13 @@ importance of those pixels masked_img = img.copy() -poly = [(404, 281), (404, 360), (359, 364), (338, 337), (145, 337), (120, 322), (145, 304), (340, 306), (362, 284)] +poly = [(404, 281), (404, 360), (359, 364), (338, 337), (145, 337), (120, 322), + (145, 304), (340, 306), (362, 284)] pr = np.array([p[0] for p in poly]) pc = np.array([p[1] for p in poly]) rr, cc = draw.polygon(pr, pc) -masked_img[rr,cc,:] = masked_img[rr,cc,:]*0.5 + hl_color*.5 +masked_img[rr, cc, :] = masked_img[rr, cc, :]*0.5 + hl_color*.5 plt.figure() plt.title('Object Marked') diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index c2ad8bfa..41ea1d75 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -41,14 +41,14 @@ cdef _preprocess_image(cnp.double_t[:, :, ::1] energy_img, for offset in range(-1, 2): c_idx = c + offset - if (c_idx > cols - 1) or (c_idx < 0) : + if (c_idx > cols - 1) or (c_idx < 0): continue if cumulative_img[r-1, c_idx] < min_cost: min_cost = cumulative_img[r-1, c_idx] track_img[r, c] = offset - cumulative_img[r,c] = min_cost + energy_img[r, c, 0] + cumulative_img[r, c] = min_cost + energy_img[r, c, 0] cdef cnp.uint8_t _mark_seam(cnp.int8_t[:, ::1] track_img, Py_ssize_t start_index, @@ -98,7 +98,7 @@ cdef cnp.uint8_t _mark_seam(cnp.int8_t[:, ::1] track_img, return 1 cdef _remove_seam(cnp.double_t[:, :, ::1] img, - cnp.uint8_t[:, ::1] seam_map, Py_ssize_t cols): + cnp.uint8_t[:, ::1] seam_map, Py_ssize_t cols): """ Removes marked seams from an image. Parameters @@ -121,6 +121,7 @@ cdef _remove_seam(cnp.double_t[:, :, ::1] img, for ch in range(channels): img[r, c, ch] = img[r, c + shift, ch] + def _seam_carve_v(img, energy_map, iters, border): """ Carve vertical seams off an image. @@ -160,7 +161,8 @@ def _seam_carve_v(img, energy_map, iters, border): cdef cnp.double_t[::1] last_row = last_row_obj cdef Py_ssize_t[::1] sorted_indices - cdef cnp.uint8_t[:, ::1] seam_map = np.zeros(img.shape[0:2], dtype=np.uint8) + cdef cnp.uint8_t[:, ::1] seam_map = np.zeros(img.shape[0:2], + dtype=np.uint8) cdef Py_ssize_t cols = img.shape[1] cdef Py_ssize_t rows = img.shape[0] cdef Py_ssize_t seams_left = iters @@ -169,7 +171,8 @@ def _seam_carve_v(img, energy_map, iters, border): cdef cnp.double_t[:, :, ::1] image = img cdef cnp.int8_t[:, ::1] track_img = np.zeros(img.shape[0:2], dtype=np.int8) - cdef cnp.double_t[:, ::1] cumulative_img = np.zeros(img.shape[0:2], dtype=np.float) + cdef cnp.double_t[:, ::1] cumulative_img = np.zeros(img.shape[0:2], + dtype=np.float) cdef cnp.double_t[:, :, ::1] energy_img energy_map[:, 0:border] = DBL_MAX From 17dc21347710f8b2a28eac924bc3dd61e8c8703a Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 11 Jun 2015 00:26:57 +0530 Subject: [PATCH 18/26] pep8 changes --- skimage/data/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index 98fb4e5f..a2ea2f33 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -243,6 +243,7 @@ def hubble_deep_field(): """ return load("hubble_deep_field.jpg") + def rocket(): """Launch photo of DSCOVR on Falcon 9 by SpaceX. From 3f9c800514daf1e3fe1a1b8e0560b54cae3cd6f9 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 11 Jun 2015 00:47:13 +0530 Subject: [PATCH 19/26] Modfied test to use new API and added code to remove remaining seams if any --- skimage/transform/_seam_carving.pyx | 2 ++ skimage/transform/tests/test_seam_carving.py | 22 ++++---------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index 41ea1d75..7159e38a 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -202,4 +202,6 @@ def _seam_carve_v(img, energy_map, iters, border): last_row[:cols] = cumulative_img[-1, :cols] sorted_indices = np.argsort(last_row_obj) + _remove_seam(image, seam_map, cols) + return img[:, 0:cols] diff --git a/skimage/transform/tests/test_seam_carving.py b/skimage/transform/tests/test_seam_carving.py index 57df75e0..1ff0b32e 100644 --- a/skimage/transform/tests/test_seam_carving.py +++ b/skimage/transform/tests/test_seam_carving.py @@ -3,34 +3,20 @@ import numpy as np from numpy import testing -def energy(img): - if(img.ndim == 3): - img = np.ascontiguousarray(img[:, :, 0]) - return (1 - img) - - def test_seam_carving(): img = np.array([[0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 1, 0, 0, 0], [1, 0, 0, 0, 0]], dtype=np.float) + energy = 1 - img - out = transform.seam_carve(img, 'horizontal', 1, energy, border=0) + out = transform.seam_carve(img, energy, 'vertical', 1, border=0) + print out testing.assert_allclose(out, 0) img = img.T - out = transform.seam_carve(img, 'vertical', 1, energy, border=0) - testing.assert_allclose(out, 0) - - img = img.T - - img3 = np.dstack([img, img, img]) - - out = transform.seam_carve(img3, 'horizontal', 1, energy, border=0) - testing.assert_allclose(out, 0) - - out = transform.seam_carve(img3, 'vertical', 1, energy, border=0) + out = transform.seam_carve(img, energy, 'horizontal', 1, border=0) testing.assert_allclose(out, 0) From 5a869dc70e24d41b0dce5854def0a10fd33002b3 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 11 Jun 2015 00:51:52 +0530 Subject: [PATCH 20/26] Added code comments --- skimage/transform/_seam_carving.pyx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index 7159e38a..fb98d2b9 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -157,6 +157,7 @@ def _seam_carve_v(img, energy_map, iters, border): "Seam Carving for Content-Aware Image Resizing" http://www.cs.jhu.edu/~misha/ReadingSeminar/Papers/Avidan07.pdf """ + # This reference has been kept to be used for the `np.argsort` call last_row_obj = np.zeros(img.shape[1], dtype=np.float) cdef cnp.double_t[::1] last_row = last_row_obj @@ -177,6 +178,12 @@ def _seam_carve_v(img, energy_map, iters, border): energy_map[:, 0:border] = DBL_MAX energy_map[:, cols-border:cols] = DBL_MAX + + # Filters often let the boundary be `0`. If the last row is of all same + # values, the least value in then penultimate row will give 3 minimum + # enries in the last row. Hence, two successive removals will always + # intersect as the 3 least seams will share the same pixels except + # they will differ in the last row. energy_map[rows-border:rows, :] = energy_map[rows-2*border:rows-border, :] energy_map = np.ascontiguousarray(energy_map[:, :, np.newaxis]) From 557354668ef2cd4a6544aabd3a316b01decdfeab Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 11 Jun 2015 01:04:46 +0530 Subject: [PATCH 21/26] removed debug print in test --- skimage/transform/tests/test_seam_carving.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/transform/tests/test_seam_carving.py b/skimage/transform/tests/test_seam_carving.py index 1ff0b32e..b4eb8977 100644 --- a/skimage/transform/tests/test_seam_carving.py +++ b/skimage/transform/tests/test_seam_carving.py @@ -12,7 +12,6 @@ def test_seam_carving(): energy = 1 - img out = transform.seam_carve(img, energy, 'vertical', 1, border=0) - print out testing.assert_allclose(out, 0) img = img.T From 55fc07edb966b307c27318d66cd669933c16e725 Mon Sep 17 00:00:00 2001 From: Dhruv Jawali Date: Sat, 13 Jun 2015 23:25:36 +0530 Subject: [PATCH 22/26] removed reduntant continue, added Cython flags and fixed negative indexing --- skimage/transform/_seam_carving.pyx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index fb98d2b9..d8dc3945 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -1,3 +1,7 @@ +# cython: cdivision=True +# cython: boundscheck=False +# cython: nonecheck=False +# cython: wraparound=False import numpy as np cimport numpy as cnp @@ -190,7 +194,7 @@ def _seam_carve_v(img, energy_map, iters, border): energy_img = energy_map _preprocess_image(energy_img, cumulative_img, track_img, cols) - last_row[...] = cumulative_img[-1, :] + last_row[...] = cumulative_img[rows - 1, :] sorted_indices = np.argsort(last_row_obj) seam_idx = 0 @@ -199,14 +203,13 @@ def _seam_carve_v(img, energy_map, iters, border): seams_left -= 1 cols -= 1 seam_idx += 1 - continue else: seam_idx = 0 _remove_seam(image, seam_map, cols) _remove_seam(energy_img, seam_map, cols) seam_map[...] = 0 _preprocess_image(energy_img, cumulative_img, track_img, cols) - last_row[:cols] = cumulative_img[-1, :cols] + last_row[:cols] = cumulative_img[rows - 1, :cols] sorted_indices = np.argsort(last_row_obj) _remove_seam(image, seam_map, cols) From e247e3bacc2f01b96facfc51580f0bad46ae15a9 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 15 Jun 2015 20:19:52 +0530 Subject: [PATCH 23/26] Precomputation optimizations and docstring changes --- skimage/data/__init__.py | 2 +- skimage/transform/_seam_carving.pyx | 41 ++++++++++++++++------------- skimage/transform/seam_carving.py | 2 +- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index a2ea2f33..14a507d3 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -248,7 +248,7 @@ def rocket(): """Launch photo of DSCOVR on Falcon 9 by SpaceX. This is the launch photo of Falcon 9 carrying DSCOVR lifted off from - SpaceX's Launch Complex 40 at Cape Canaveral Air Force Station, Fla.. + SpaceX's Launch Complex 40 at Cape Canaveral Air Force Station, FL. Notes ----- diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index d8dc3945..f70c1e9e 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -20,10 +20,11 @@ cdef _preprocess_image(cnp.double_t[:, :, ::1] energy_img, Parameters ---------- energy_img : (M, N, 1) ndarray - The array of cost of removal of each pixel. Seam carving tries to avoid - pixels with high costs. + Cost array representing the expense to remove each pixel. Seam carving + tries to avoid pixels with high costs. cumulative_img : (M, N) ndarray - The array to be updated with the total cost of lowest energy seams. + The array to be updated inplace with the total cost of lowest energy + seams. track_img : (M, N) ndarray For each pixel, `track_img` stores the relative column offset in the previous row which has the lowest value in `cumulative_img`. This @@ -35,21 +36,24 @@ cdef _preprocess_image(cnp.double_t[:, :, ::1] energy_img, cdef Py_ssize_t r, c, offset, c_idx cdef Py_ssize_t rows = energy_img.shape[0] cdef cnp.double_t min_cost = DBL_MAX + cdef Py_ssize_t colsm1 = cols - 1 + cdef Py_ssize_t rm1 for c in range(cols): cumulative_img[0, c] = energy_img[0, c, 0] for r in range(1, rows): + rm1 = r - 1 for c in range(cols): min_cost = DBL_MAX for offset in range(-1, 2): c_idx = c + offset - if (c_idx > cols - 1) or (c_idx < 0): + if (c_idx > colsm1) or (c_idx < 0): continue - if cumulative_img[r-1, c_idx] < min_cost: - min_cost = cumulative_img[r-1, c_idx] + if cumulative_img[rm1, c_idx] < min_cost: + min_cost = cumulative_img[rm1, c_idx] track_img[r, c] = offset cumulative_img[r, c] = min_cost + energy_img[r, c, 0] @@ -87,7 +91,7 @@ cdef cnp.uint8_t _mark_seam(cnp.int8_t[:, ::1] track_img, current_seam_indices[rows - 1] = start_index for row in range(rows - 2, -1, -1): - col = current_seam_indices[row+1] + col = current_seam_indices[row + 1] offset = track_img[row, col] col = col + offset current_seam_indices[row] = col @@ -103,7 +107,7 @@ cdef cnp.uint8_t _mark_seam(cnp.int8_t[:, ::1] track_img, cdef _remove_seam(cnp.double_t[:, :, ::1] img, cnp.uint8_t[:, ::1] seam_map, Py_ssize_t cols): - """ Removes marked seams from an image. + """ Remove marked seams from an image. Parameters ---------- @@ -112,38 +116,37 @@ cdef _remove_seam(cnp.double_t[:, :, ::1] img, seam_map : (M, N) ndarray Array with seams to be removed marked by non-zero entries. cols : int - The number of colums to process. + The number of columns to process. """ cdef Py_ssize_t rows = img.shape[0] cdef Py_ssize_t channels = img.shape[2] cdef Py_ssize_t r, c, ch, shift + cdef Py_ssize_t c_shift for r in range(rows): shift = 0 for c in range(cols): shift += seam_map[r, c] + c_shift = c + shift for ch in range(channels): - img[r, c, ch] = img[r, c + shift, ch] + img[r, c, ch] = img[r, c_shift, ch] def _seam_carve_v(img, energy_map, iters, border): """ Carve vertical seams off an image. - Carves out vertical seams off an image while using the given energy - map to decide the importance of each pixel.[1] + Carves out vertical seams from an image while using the given energy map to + decide the importance of each pixel.[1]_ Parameters ---------- img : (M, N) or (M, N, 3) ndarray Input image whose vertical seams are to be removed. - iters : int - Number of vertical seams are to be removed. energy_map : (M, N) ndarray - The array to decide the importance of each pixel. The higher - the value corresponding to a pixel, the more the algorithm will try - to keep it in the image. - num : int - Number of seams are to be removed. + Cost array denoting importance of each pixel. The algorithm will try to + retain high valued pixels. + iters : int + Number of vertical seams to be removed. border : int, optional The number of pixels in the right, left and bottom end of the image to be excluded from being considered for a seam. This is important as diff --git a/skimage/transform/seam_carving.py b/skimage/transform/seam_carving.py index e037b6e1..95e975ce 100644 --- a/skimage/transform/seam_carving.py +++ b/skimage/transform/seam_carving.py @@ -7,7 +7,7 @@ import numpy as np def seam_carve(img, energy_map, mode, num, border=1, force_copy=True): """ Carve vertical or horizontal seams off an image. - Carves out vertical/horizontal seams off an image while using the given + Carves out vertical/horizontal seams from an image while using the given energy map to decide the importance of each pixel. Parameters From 958f410a5c55e124f9ae10a9724104eb03e0691b Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 15 Jun 2015 20:47:24 +0530 Subject: [PATCH 24/26] Changes example explanation text --- doc/examples/plot_seam_carving.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/examples/plot_seam_carving.py b/doc/examples/plot_seam_carving.py index c5250d32..f1dcb948 100644 --- a/doc/examples/plot_seam_carving.py +++ b/doc/examples/plot_seam_carving.py @@ -4,9 +4,10 @@ Seam Carving ============ This example demonstrates how images can be resized using seam carving [1]_. -Resizing often distorts contents in the image. Seam carving tries to resize -images while trying to keep important content intact. In this example we are -using the Sobel filter to signify the importance of each pixel. +Resizing to a new aspect ratio distorts image contents. Seam carving attempts +to resize *without* distortion, by removing regions of an image which are less +important. In this example we are using the Sobel filter to signify the +importance of each pixel. .. [1] Shai Avidan and Ariel Shamir "Seam Carving for Content-Aware Image Resizing" From fee667b35cbac9165cbdbbd797a5909d8959927a Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 15 Jun 2015 20:49:26 +0530 Subject: [PATCH 25/26] preallocated seam_buffer and added noil --- skimage/transform/_seam_carving.pyx | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index f70c1e9e..9fe0789a 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -8,10 +8,10 @@ cimport numpy as cnp cdef cnp.double_t DBL_MAX = np.finfo(np.double).max -cdef _preprocess_image(cnp.double_t[:, :, ::1] energy_img, - cnp.double_t[:, ::1] cumulative_img, - cnp.int8_t[:, ::1] track_img, - Py_ssize_t cols): +cdef void _preprocess_image(cnp.double_t[:, :, ::1] energy_img, + cnp.double_t[:, ::1] cumulative_img, + cnp.int8_t[:, ::1] track_img, + Py_ssize_t cols) nogil: """ For each row, compute the lowest seam value for all its columns. This function updates `cumulative_img` such that `cumulative_img[r, c]` @@ -60,7 +60,8 @@ cdef _preprocess_image(cnp.double_t[:, :, ::1] energy_img, cdef cnp.uint8_t _mark_seam(cnp.int8_t[:, ::1] track_img, Py_ssize_t start_index, - cnp.uint8_t[:, ::1] seam_map): + cnp.uint8_t[:, ::1] seam_map, + Py_ssize_t[::1] seam_buffer) nogil: """ Re-trace the optimal seam from a given column in the last row. @@ -77,6 +78,9 @@ cdef cnp.uint8_t _mark_seam(cnp.int8_t[:, ::1] track_img, seam_map : (M, N) ndarray The array used to mark seams. If a pixel is marked as as seam it is set to `1`, else `0`. + seam_buffer : (M,) ndarray + Buffer used to store the column indices of the seam currently being + checked. This is preallocated to save time. Returns ------- @@ -84,7 +88,7 @@ cdef cnp.uint8_t _mark_seam(cnp.int8_t[:, ::1] track_img, `1` if seam was marked, `0` is seam inersects and was not marked. """ cdef Py_ssize_t rows = track_img.shape[0] - cdef Py_ssize_t[::1] current_seam_indices = np.zeros(rows, dtype=np.int) + cdef Py_ssize_t[::1] current_seam_indices = seam_buffer cdef Py_ssize_t row, col cdef cnp.int8_t offset cdef Py_ssize_t seams @@ -105,8 +109,8 @@ cdef cnp.uint8_t _mark_seam(cnp.int8_t[:, ::1] track_img, return 1 -cdef _remove_seam(cnp.double_t[:, :, ::1] img, - cnp.uint8_t[:, ::1] seam_map, Py_ssize_t cols): +cdef void _remove_seam(cnp.double_t[:, :, ::1] img, + cnp.uint8_t[:, ::1] seam_map, Py_ssize_t cols) nogil: """ Remove marked seams from an image. Parameters @@ -176,6 +180,7 @@ def _seam_carve_v(img, energy_map, iters, border): cdef Py_ssize_t seams_left = iters cdef Py_ssize_t seams_removed cdef Py_ssize_t seam_idx + cdef Py_ssize_t[::1] seam_buffer = np.zeros(rows, dtype=np.int) cdef cnp.double_t[:, :, ::1] image = img cdef cnp.int8_t[:, ::1] track_img = np.zeros(img.shape[0:2], dtype=np.int8) @@ -202,7 +207,8 @@ def _seam_carve_v(img, energy_map, iters, border): seam_idx = 0 while seams_left > 0: - if _mark_seam(track_img, sorted_indices[seam_idx], seam_map): + if _mark_seam(track_img, sorted_indices[seam_idx], seam_map, + seam_buffer): seams_left -= 1 cols -= 1 seam_idx += 1 From 85946c8eed61417a5da8555136fe2438e4c68fc8 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 16 Jun 2015 19:48:14 +0530 Subject: [PATCH 26/26] Changed return type to bint and improved code comments --- skimage/transform/_seam_carving.pyx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/skimage/transform/_seam_carving.pyx b/skimage/transform/_seam_carving.pyx index 9fe0789a..3499514c 100644 --- a/skimage/transform/_seam_carving.pyx +++ b/skimage/transform/_seam_carving.pyx @@ -58,10 +58,10 @@ cdef void _preprocess_image(cnp.double_t[:, :, ::1] energy_img, cumulative_img[r, c] = min_cost + energy_img[r, c, 0] -cdef cnp.uint8_t _mark_seam(cnp.int8_t[:, ::1] track_img, - Py_ssize_t start_index, - cnp.uint8_t[:, ::1] seam_map, - Py_ssize_t[::1] seam_buffer) nogil: +cdef bint _mark_seam(cnp.int8_t[:, ::1] track_img, + Py_ssize_t start_index, + cnp.uint8_t[:, ::1] seam_map, + Py_ssize_t[::1] seam_buffer) nogil: """ Re-trace the optimal seam from a given column in the last row. @@ -84,8 +84,8 @@ cdef cnp.uint8_t _mark_seam(cnp.int8_t[:, ::1] track_img, Returns ------- - out : int - `1` if seam was marked, `0` is seam inersects and was not marked. + success : int + `1` if seam was marked, `0` is seam intersects and was not marked. """ cdef Py_ssize_t rows = track_img.shape[0] cdef Py_ssize_t[::1] current_seam_indices = seam_buffer @@ -191,11 +191,11 @@ def _seam_carve_v(img, energy_map, iters, border): energy_map[:, 0:border] = DBL_MAX energy_map[:, cols-border:cols] = DBL_MAX - # Filters often let the boundary be `0`. If the last row is of all same - # values, the least value in then penultimate row will give 3 minimum - # enries in the last row. Hence, two successive removals will always - # intersect as the 3 least seams will share the same pixels except - # they will differ in the last row. + # Filters often let the boundary be `0`. If all the entries in the last + # row of `energy_img` are equal, the minimum value in the penultimate row + # of `cumulative_img` will result in 3 minimum values in its last row. + # Hence, two successive removals will always intersect as the 3 least seams + # will share the same pixels except they will differ in the last row. energy_map[rows-border:rows, :] = energy_map[rows-2*border:rows-border, :] energy_map = np.ascontiguousarray(energy_map[:, :, np.newaxis])