From d30ed259688bd06c3ec4038f8739f9fb407327df Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 28 Mar 2015 21:04:21 +0530 Subject: [PATCH] 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__':