├── .circleci └── config.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE.txt ├── Makefile ├── README.md ├── demo ├── .ipynb_checkpoints │ └── Seam Carving Visual Demos-checkpoint.ipynb ├── Seam Carving Visual Demos.ipynb ├── castle_small.jpg ├── castle_small_shrunk.png ├── cat.png ├── cat_eng_color.png ├── cat_eng_grad.png ├── cat_eng_total.png ├── cat_grown.png ├── cat_out.png ├── cat_shrunk.png ├── hot_dog.jpg ├── hot_dog.png ├── lotr.jpg ├── lotr_out.png └── wyeth.jpg ├── requirements.txt ├── seam_carver ├── __init__.py ├── seam_carver.py └── seam_carver_test.py └── setup.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Python CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-python/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` 11 | - image: circleci/python:3.6.1 12 | 13 | # Specify service dependencies here if necessary 14 | # CircleCI maintains a library of pre-built images 15 | # documented at https://circleci.com/docs/2.0/circleci-images/ 16 | # - image: circleci/postgres:9.4 17 | 18 | working_directory: ~/repo 19 | 20 | steps: 21 | - checkout 22 | 23 | # Download and cache dependencies 24 | - restore_cache: 25 | keys: 26 | - v1-dependencies-{{ checksum "requirements.txt" }} 27 | # fallback to using the latest cache if no exact match is found 28 | - v1-dependencies- 29 | 30 | - run: 31 | name: install dependencies 32 | command: | 33 | python3 -m venv venv 34 | . venv/bin/activate 35 | pip install -r requirements.txt 36 | 37 | - save_cache: 38 | paths: 39 | - ./venv 40 | key: v1-dependencies-{{ checksum "requirements.txt" }} 41 | 42 | # run tests! 43 | - run: 44 | name: run tests 45 | command: | 46 | . venv/bin/activate 47 | make test 48 | 49 | - store_artifacts: 50 | path: test-reports 51 | destination: test-reports 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Environments 7 | .env 8 | .venv 9 | env/ 10 | venv/ 11 | ENV/ 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | #OSX 32 | .DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "${workspaceRoot}/env/bin/python", 3 | "python.linting.pylintEnabled": false 4 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Dylan Harness 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: demo 2 | 3 | 4 | clean-pyc: 5 | find . | grep -E "(__pycache__|\.pyc|\.pyo$\)" | xargs rm -rf 6 | 7 | lint: 8 | python -m pylint ./seam_carver.py 9 | 10 | test: 11 | python seam_carver/seam_carver_test.py 12 | 13 | build: 14 | python setup.py sdist bdist_wheel 15 | 16 | clean-build: 17 | rm -rf dist 18 | rm -rf build 19 | rm -rf seam_carver.egg-info 20 | 21 | publish: 22 | twine upload dist/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # seam_carver 6 | Python Seam Carving module 7 | 8 | seam_carver is a small tool for retargetting images to any dimension greater or smaller. It uses the process of seam carving as originally described by Shai Avidan & Ariel Shamir in http://perso.crans.org/frenoy/matlab2012/seamcarving.pdf 9 | 10 | A combination of the gradient energy (determined with the sobel filter) and a simple color energy is used to determine the least important seam. Additon of seams occurs by the same mechanism as described in the paper. 11 | 12 | ### Installation 13 | 14 | ``` 15 | pip install seam_carver 16 | ``` 17 | 18 | ### Basic Usage 19 | ``` python 20 | from scipy import misc 21 | import numpy as np 22 | from seam_carver import intelligent_resize 23 | 24 | rgb_weights = [-3, 1, -3] 25 | mask_weight = 10 26 | cat_img = misc.imread('./demo/cat.png') 27 | mask = np.zeros(cat_img.shape) 28 | 29 | resized_img = intelligent_resize(cat_img, 0, -20, rgb_weights, mask, mask_weight) 30 | misc.imsave('./demo/cat_shrunk.png', resized_img) 31 | ``` 32 | 33 | ### Options 34 | 35 | ``` python 36 | def intelligent_resize(img, d_rows, d_columns, rgb_weights, mask, mask_weight): 37 | """ 38 | Changes the size of the provided image in either the vertical or horizontal direction, 39 | by increasing or decreasing or some combination of the two. 40 | 41 | Args: 42 | img (n,m,3 numpy matrix): RGB image to be resized. 43 | d_rows (int): The change (delta) in rows. Positive number indicated insertions, negative is removal. 44 | d_columns (int): The change (delta) in columns. Positive number indicated insertions, negative is removal. 45 | rgb_weights (1,3 numpy matrix): Additional weight paramater to be applied to pixels. 46 | mask (n,m,3 numpy matrix): Mask matrix indicating areas to make more or less likely for removal. 47 | mask_weight (int): Scalar multiple to be applied to mask. 48 | 49 | Returns: 50 | n,m,3 numpy matrix: Resized RGB image. 51 | """ 52 | ``` 53 | 54 | ### Examples 55 | 56 | For more examples and details see the [demo jupyter notebook](https://github.com/dharness/seam_carving/blob/dharness/improving_docs/demo/Seam%20Carving%20Visual%20Demos.ipynb) 57 | 58 | **Original** 59 | 60 | ![Alt text](/demo/lotr.jpg?raw=true) 61 | 62 | **Resized** 63 | 64 | ![Alt text](/demo/lotr_out.png?raw=true) 65 | 66 | 67 | **Original** 68 | 69 | ![Alt text](/demo/hot_dog.jpg?raw=true) 70 | 71 | **Resized** 72 | 73 | ![Alt text](/demo/hot_dog.png?raw=true) 74 | 75 | ### Limitations 76 | 77 | **Original** 78 | 79 | ![Alt text](/demo/cat.png?raw=true) 80 | 81 | **Resized** 82 | 83 | ![Alt text](/demo/cat_out.png?raw=true) -------------------------------------------------------------------------------- /demo/castle_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/castle_small.jpg -------------------------------------------------------------------------------- /demo/castle_small_shrunk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/castle_small_shrunk.png -------------------------------------------------------------------------------- /demo/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/cat.png -------------------------------------------------------------------------------- /demo/cat_eng_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/cat_eng_color.png -------------------------------------------------------------------------------- /demo/cat_eng_grad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/cat_eng_grad.png -------------------------------------------------------------------------------- /demo/cat_eng_total.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/cat_eng_total.png -------------------------------------------------------------------------------- /demo/cat_grown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/cat_grown.png -------------------------------------------------------------------------------- /demo/cat_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/cat_out.png -------------------------------------------------------------------------------- /demo/cat_shrunk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/cat_shrunk.png -------------------------------------------------------------------------------- /demo/hot_dog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/hot_dog.jpg -------------------------------------------------------------------------------- /demo/hot_dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/hot_dog.png -------------------------------------------------------------------------------- /demo/lotr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/lotr.jpg -------------------------------------------------------------------------------- /demo/lotr_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/lotr_out.png -------------------------------------------------------------------------------- /demo/wyeth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dharness/seam_carving/45ec3bdbdb5c88d64d100fc52cc98578b0e319c6/demo/wyeth.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appnope==0.1.0 2 | astroid==1.5.3 3 | autopep8==1.3.2 4 | bleach==2.0.0 5 | certifi==2017.7.27.1 6 | chardet==3.0.4 7 | cycler==0.10.0 8 | decorator==4.1.2 9 | entrypoints==0.2.3 10 | html5lib==0.999999999 11 | idna==2.6 12 | ipykernel==4.6.1 13 | ipython==6.1.0 14 | ipython-genutils==0.2.0 15 | ipywidgets==7.0.0 16 | isort==4.2.15 17 | jedi==0.10.2 18 | Jinja2==2.9.6 19 | jsonschema==2.6.0 20 | jupyter==1.0.0 21 | jupyter-client==5.1.0 22 | jupyter-console==5.2.0 23 | jupyter-core==4.3.0 24 | lazy-object-proxy==1.3.1 25 | MarkupSafe==1.0 26 | matplotlib==2.0.2 27 | mccabe==0.6.1 28 | mistune==0.7.4 29 | nbconvert==5.2.1 30 | nbformat==4.4.0 31 | notebook==5.0.0 32 | numpy==1.13.1 33 | olefile==0.44 34 | pandocfilters==1.4.2 35 | pep8==1.7.0 36 | pexpect==4.2.1 37 | pickleshare==0.7.4 38 | Pillow==4.2.1 39 | pkginfo==1.4.1 40 | prompt-toolkit==1.0.15 41 | ptyprocess==0.5.2 42 | pycodestyle==2.3.1 43 | Pygments==2.2.0 44 | pylint==1.7.2 45 | pyparsing==2.2.0 46 | python-dateutil==2.6.1 47 | pytz==2017.2 48 | pyzmq==16.0.2 49 | qtconsole==4.3.1 50 | requests==2.18.4 51 | requests-toolbelt==0.8.0 52 | scipy==0.19.1 53 | simplegeneric==0.8.1 54 | six==1.10.0 55 | terminado==0.6 56 | testpath==0.3.1 57 | tornado==4.5.1 58 | tqdm==4.15.0 59 | traitlets==4.3.2 60 | twine==1.9.1 61 | urllib3==1.22 62 | wcwidth==0.1.7 63 | webencodings==0.5.1 64 | widgetsnbextension==3.0.2 65 | wrapt==1.10.11 66 | -------------------------------------------------------------------------------- /seam_carver/__init__.py: -------------------------------------------------------------------------------- 1 | from .seam_carver import intelligent_resize -------------------------------------------------------------------------------- /seam_carver/seam_carver.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides Utilities for seam carving 3 | """ 4 | from scipy import signal 5 | from scipy.ndimage.filters import gaussian_gradient_magnitude 6 | from scipy.ndimage import sobel, generic_gradient_magnitude 7 | import numpy as np 8 | 9 | 10 | def normalize(img, max_value=255.0): 11 | """ 12 | Normalizes all values in the provided image to lie between 0 and 13 | the provided max value 14 | 15 | Args: 16 | img (n,m numpy matrix): RGB image. 17 | max_value (float): upper bound for normalization 18 | 19 | Returns: 20 | n,m,3 numpy matrix: Normalized img 21 | """ 22 | mins = np.min(img) 23 | normalized = np.array(img) + np.abs(mins) 24 | maxs = np.max(normalized) 25 | normalized *= (max_value / maxs) 26 | 27 | return normalized 28 | 29 | 30 | def rgb_to_gray(img): 31 | """ 32 | Converts RGB image to greyscale 33 | 34 | Args: 35 | img (n,m,3 numpy matrix): RGB image 36 | 37 | Returns: 38 | n,m numpy matrix: Greyscale image 39 | """ 40 | r, g, b = img[:, :, 0], img[:, :, 1], img[:, :, 2] 41 | gray = 0.2989 * r + 0.5870 * g + 0.1140 * b 42 | return gray 43 | 44 | 45 | def compute_eng_grad(img): 46 | """ 47 | Computes the energy of an image using gradient magnitude 48 | 49 | Args: 50 | img4 (n,m,4 numpy matrix): RGB image with additional mask layer. 51 | rgb_weights (n,m numpy matrix): img-specific weights for RBG values 52 | 53 | Returns: 54 | n,m numpy matrix: Gradient energy map of the provided image 55 | """ 56 | bw_img = rgb_to_gray(img) 57 | eng = generic_gradient_magnitude(bw_img, sobel) 58 | eng = gaussian_gradient_magnitude(bw_img, 1) 59 | return normalize(eng) 60 | 61 | 62 | def compute_eng_color(img, rgb_weights): 63 | """ 64 | Computes the energy of an image using its color properties 65 | 66 | Args: 67 | img4 (n,m,4 numpy matrix): RGB image with additional mask layer. 68 | rgb_weights (n,m numpy matrix): img-specific weights for RBG values 69 | 70 | Returns: 71 | n,m numpy matrix: Color energy map of the provided image 72 | """ 73 | eng = np.dstack(( 74 | img[:, :, 0] * rgb_weights[0], 75 | img[:, :, 1] * rgb_weights[1], 76 | img[:, :, 2] * rgb_weights[2] 77 | )) 78 | eng = np.sum(eng, axis=2) 79 | return eng 80 | 81 | 82 | def compute_eng(img4, rgb_weights, mask_weight): 83 | """ 84 | Computes total energy map of the provided image 85 | 86 | Args: 87 | img4 (n,m,4 numpy matrix): RGB image with additional mask layer. 88 | rgb_weights (n,m numpy matrix): img-specific weights for RBG values 89 | mask_weight (float): Scalar multiplier for the mask. 90 | 91 | Returns: 92 | tuple ( 93 | n,m numpy matrix: Total energy map of the provided image 94 | ) 95 | """ 96 | img = img4[:, :, 0:3] 97 | mask = img4[:, :, 3] 98 | eng_color = compute_eng_color(img, rgb_weights) 99 | eng_grad = compute_eng_grad(img) 100 | eng_mask = mask * mask_weight 101 | return eng_grad + eng_color + eng_mask 102 | 103 | 104 | def remove_seam(img4, seam): 105 | """ 106 | Removes 1 seam from the image either vertical or horizontal 107 | 108 | Args: 109 | img4 (n,m,4 numpy matrix): RGB image with additional mask layer. 110 | seam (n,m numpy matrix): Seam to indicate position for removal 111 | 112 | Returns: 113 | tuple ( 114 | n,m-1,4 numpy matrix: Image with seam removed from all layers 115 | ) 116 | """ 117 | width = img4.shape[0] if img4.shape[0] == seam.shape[0] else img4.shape[0] - 1 118 | height = img4.shape[1] if img4.shape[1] == seam.shape[1] else img4.shape[1] - 1 119 | new_img = np.zeros(( 120 | width, 121 | height, 122 | img4.shape[2], 123 | )) 124 | for i, seam_row in enumerate(seam): 125 | img_row = img4[i] 126 | for col in seam_row: 127 | img_row = np.delete(img_row, col.astype(int), axis=0) 128 | new_img[i] = img_row 129 | 130 | return new_img 131 | 132 | 133 | def find_seams(eng): 134 | """ 135 | Adds the provided seam in either the horizontal or verical direction 136 | 137 | Args: 138 | eng (n,m numpy matrix): Energy map of an image 139 | 140 | Returns: 141 | tuple ( 142 | n,m numpy matrix: Matrixof the cummulative energy along a path 143 | n,m numpy matrix:: History of parents along a given path in M 144 | ) 145 | """ 146 | rows = len(eng) 147 | cols = len(eng[0]) 148 | M = np.zeros(shape=(rows, cols)) 149 | P = np.zeros(shape=(rows, cols)) 150 | M[0] = eng[0] 151 | P[0] = [-1] * cols 152 | inf = float('Inf') 153 | 154 | for r in range(1, rows): 155 | for c in range(0, cols): 156 | option_1 = M[r - 1, c - 1] if (c > 0) else inf 157 | option_2 = M[r - 1, c] if (c < cols) else inf 158 | option_3 = M[r - 1, c + 1] if (c < cols - 1) else inf 159 | 160 | if (option_1 <= option_2 and option_1 <= option_3): 161 | M[r, c] = eng[r, c] + M[r - 1, c - 1] 162 | P[r, c] = c - 1 163 | elif (option_2 <= option_1 and option_2 <= option_3): 164 | M[r, c] = eng[r, c] + M[r - 1, c] 165 | P[r, c] = c 166 | else: 167 | M[r, c] = eng[r, c] + M[r - 1, c + 1] 168 | P[r, c] = c + 1 169 | 170 | P = P.astype(int) 171 | return (M, P) 172 | 173 | 174 | def get_best_seam(M, P): 175 | """ 176 | Determines the best vertical seam based on the cummulative energy in M 177 | 178 | Args: 179 | M (n,m numpy matrix): Cumulative energy matrix of best seams 180 | P (n,m numpy matrix): Path history of best seam in cumulative energy matrix 181 | 182 | Returns: 183 | tuple ( 184 | n,1 numpy matrix: Positions of vertical seam to be removed 185 | float: Total cost/energy of lowest energy seam 186 | ) 187 | """ 188 | rows = len(P) 189 | seam = np.zeros((rows, 1)) 190 | i = M[-1].argmin(axis=0) 191 | cost = M[-1][i] 192 | seam[rows - 1] = i 193 | for r in reversed(range(0, rows)): 194 | seam[r][0] = i 195 | i = P[r][i] 196 | return (seam, cost) 197 | 198 | 199 | def add_seam(img4, seam, eng): 200 | """ 201 | Adds the provided seam in either the horizontal or verical direction 202 | 203 | Args: 204 | img4 (n,m,4 numpy matrix): RGB image with additional mask layer. 205 | seam (n,m numpy matrix): Seam to indicate position for insertion 206 | eng (n,m numpy matrix): Pre-computed energy matrix for supplied image. 207 | 208 | Returns: 209 | tuple ( 210 | n,m+1,4 numpy matrix: Image with seam added to all layers 211 | n,m+1,4 numpy matrix: Energy matrix with high weight seam added in position of insertion 212 | ) 213 | """ 214 | width = img4.shape[0] if img4.shape[0] == seam.shape[0] else img4.shape[0] + 1 215 | height = img4.shape[1] if img4.shape[1] == seam.shape[1] else img4.shape[1] + 1 216 | new_img = np.zeros(( 217 | width, 218 | height, 219 | img4.shape[2], 220 | )) 221 | highest_eng = np.max(eng) 222 | 223 | for i, seam_row in enumerate(seam): 224 | img_row = img4[i] 225 | for col in seam_row.astype(int): 226 | pixels = np.array([img_row[col]]) 227 | if col > 0: pixels = np.dstack((pixels, img_row[col-1])) 228 | if col < len(img_row)-1: pixels = np.dstack((pixels, img_row[col+1])) 229 | new_pixel = np.mean(pixels, axis=2)[0] 230 | eng[i, col] = highest_eng 231 | img_row = np.insert(img_row, col + 1, new_pixel, axis=0) 232 | new_img[i] = img_row 233 | 234 | return new_img, eng 235 | 236 | 237 | def reduce_width(img4, eng): 238 | """ 239 | Reduces the width by 1 pixel 240 | 241 | Args: 242 | img4 (n,m,4 numpy matrix): RGB image with additional mask layer. 243 | eng (n,m numpy matrix): Pre-computed energy matrix for supplied image. 244 | 245 | Returns: 246 | tuple ( 247 | n,1 numpy matrix: the added seam, 248 | n,m-1,4 numpy matrix: The width-redcued image, 249 | float: The cost of the seam removed 250 | ) 251 | """ 252 | M, P = find_seams(eng) 253 | seam, cost = get_best_seam(M, P) 254 | reduced_img4 = remove_seam(img4, seam) 255 | return seam, reduced_img4, cost 256 | 257 | 258 | def reduce_height(img4, eng): 259 | """ 260 | Reduces the height by 1 pixel 261 | 262 | Args: 263 | img4 (n,m,4 numpy matrix): RGB image with additional mask layer. 264 | eng (n,m numpy matrix): Pre-computed energy matrix for supplied image. 265 | 266 | Returns: 267 | tuple ( 268 | n,1 numpy matrix: the removed seam, 269 | n-1,m,4 numpy matrix: The height-redcued image, 270 | float: The cost of the seam removed 271 | ) 272 | """ 273 | flipped_eng = np.transpose(eng) 274 | flipped_img4 = np.transpose(img4, (1, 0, 2)) 275 | flipped_seam, reduced_flipped_img4, cost = reduce_width(flipped_img4, flipped_eng) 276 | return ( 277 | np.transpose(flipped_seam), 278 | np.transpose(reduced_flipped_img4, (1, 0, 2)), 279 | cost 280 | ) 281 | 282 | 283 | def increase_width(img4, eng): 284 | """ 285 | Increase the width by 1 pixel 286 | Args: 287 | img4 (n,m,4 numpy matrix): RGB image with additional mask layer. 288 | eng (n,m numpy matrix): Pre-computed energy matrix for supplied image. 289 | 290 | Returns: 291 | tuple ( 292 | n,1 numpy matrix: the added seam, 293 | n,m+1,4 numpy matrix: The width-increased image, 294 | float: The cost of the seam added, 295 | n,m+1 numpy matrix: Energy matrix with high-weight seam added 296 | ) 297 | """ 298 | M, P = find_seams(eng) 299 | seam, cost = get_best_seam(M, P) 300 | increased_img4, increased_eng = add_seam(img4, seam, eng) 301 | return ( 302 | seam, 303 | increased_img4, 304 | cost, 305 | increased_eng 306 | ) 307 | 308 | 309 | def increase_height(img4, eng): 310 | """ 311 | Increase the height by 1 pixel 312 | 313 | Args: 314 | img4 (n,m,4 numpy matrix): RGB image with additional mask layer. 315 | eng (n,m numpy matrix): Pre-computed energy matrix for supplied image. 316 | 317 | Returns: 318 | tuple ( 319 | n,1 numpy matrix: the added seam, 320 | n+1,m,4 numpy matrix: The height-increased image, 321 | float: The cost of the seam added, 322 | n+1,m numpy matrix: Energy matrix with high-weight seam added 323 | ) 324 | """ 325 | flipped_eng = np.transpose(eng) 326 | flipped_img4 = np.transpose(img4, (1, 0, 2)) 327 | M, P = find_seams(flipped_eng) 328 | flipped_seam, cost = get_best_seam(M, P) 329 | increased_fliped_img4, increased_fliped_eng = add_seam(flipped_img4, flipped_seam, flipped_eng) 330 | return ( 331 | np.transpose(flipped_seam), 332 | np.transpose(increased_fliped_img4, (1, 0, 2)), 333 | cost, 334 | np.transpose(increased_fliped_eng) 335 | ) 336 | 337 | 338 | def intelligent_resize(img, d_rows, d_columns, rgb_weights, mask, mask_weight): 339 | """ 340 | Changes the size of the provided image in either the vertical or horizontal direction, 341 | by increasing or decreasing or some combination of the two. 342 | 343 | Args: 344 | img (n,m,3 numpy matrix): RGB image to be resized. 345 | d_rows (int): The change (delta) in rows. Positive number indicated insertions, negative is removal. 346 | d_columns (int): The change (delta) in columns. Positive number indicated insertions, negative is removal. 347 | rgb_weights (1,3 numpy matrix): Additional weight paramater to be applied to pixels. 348 | mask (n,m,3 numpy matrix): Mask matrix indicating areas to make more or less likely for removal. 349 | mask_weight (int): Scalar multiple to be applied to mask. 350 | 351 | Returns: 352 | n,m,3 numpy matrix: Resized RGB image. 353 | """ 354 | img4 = np.dstack((img, mask)) 355 | is_increase_width = d_columns > 0 356 | is_increase_height = d_rows > 0 357 | d_rows = np.abs(d_rows) 358 | d_columns = np.abs(d_columns) 359 | adjusted_width_energy = None 360 | adjusted_height_energy = None 361 | 362 | while(d_rows > 0 or d_columns > 0): 363 | if(d_columns > 0 and not is_increase_width): 364 | eng = compute_eng(img4, rgb_weights, mask_weight) 365 | seam, adjusted_img4, cost = reduce_width(img4, eng) 366 | img4 = adjusted_img4 367 | d_columns = d_columns - 1 368 | elif(d_columns > 0 and is_increase_width): 369 | eng = adjusted_width_energy 370 | if (adjusted_width_energy is None): 371 | eng = compute_eng(img4, rgb_weights, mask_weight) 372 | seam, adjusted_img4, cost, adjusted_width_energy = increase_width(img4, eng) 373 | img4 = adjusted_img4 374 | d_columns = d_columns - 1 375 | 376 | if(d_rows > 0 and not is_increase_height): 377 | eng = compute_eng(img4, rgb_weights, mask_weight) 378 | seam, adjusted_img4, cost = reduce_height(img4, eng) 379 | img4 = adjusted_img4 380 | d_rows = d_rows - 1 381 | elif(d_rows > 0 and is_increase_height): 382 | eng = adjusted_height_energy 383 | if (adjusted_height_energy is None): 384 | eng = compute_eng(img4, rgb_weights, mask_weight) 385 | seam, adjusted_img4, cost, adjusted_height_energy = increase_height(img4, eng) 386 | img4 = adjusted_img4 387 | d_rows = d_rows - 1 388 | 389 | return img4[:,:,0:3] 390 | -------------------------------------------------------------------------------- /seam_carver/seam_carver_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | from seam_carver import ( 5 | normalize, 6 | compute_eng_grad, 7 | compute_eng_color, 8 | compute_eng, 9 | remove_seam, 10 | add_seam, 11 | find_seams, 12 | get_best_seam, 13 | reduce_width, 14 | reduce_height, 15 | increase_width, 16 | increase_height, 17 | intelligent_resize 18 | ) 19 | 20 | 21 | class TestSeamCarver(unittest.TestCase): 22 | 23 | @classmethod 24 | def setUpClass(self): 25 | img4 = np.zeros((5,5,4)) 26 | img4[:,:,0] = np.vstack(( 27 | [101, 244, 231, 126, 249], 28 | [151, 249, 219, 9, 64], 29 | [88, 93, 21, 112, 155], 30 | [114, 55, 55, 120, 205], 31 | [84, 154, 24, 252, 63] 32 | )) 33 | 34 | img4[:,:,1] = np.vstack(( 35 | [115, 228, 195, 68, 102], 36 | [92, 74, 216, 64, 221], 37 | [218, 134, 123, 35, 213], 38 | [229, 23, 192, 111, 147], 39 | [164, 218, 78, 231, 146] 40 | )) 41 | 42 | img4[:,:,2] = np.vstack(( 43 | [91, 201, 137, 85, 182], 44 | [225, 102, 91, 122, 60], 45 | [85, 46, 139, 162, 241], 46 | [101, 252, 31, 100, 69], 47 | [158, 198, 196, 26, 239] 48 | )) 49 | 50 | img4[:,:,3] = np.vstack(( 51 | [-1, 1, 0, -1, 0], 52 | [1, 1, 0, -1, 1], 53 | [0, 1, 0, -1, -1], 54 | [1, 1, 0, -1, 1], 55 | [-1, -1, 0, 1, 0] 56 | )) 57 | self.img4 = img4 58 | 59 | 60 | def test_normalize(self): 61 | img = np.array([ 62 | [-100, 244, 231, 126, 249], 63 | [151, 249, 219, 9, 64], 64 | [88, 93, 21, 112, 155], 65 | [114, 55, 55, 120, 205], 66 | [84, 154, 24, 252, 500] 67 | ]).astype(float) 68 | 69 | normalized = normalize(img) 70 | self.assertLessEqual(np.max(normalized), 255) 71 | self.assertGreaterEqual(np.min(normalized), 0) 72 | 73 | 74 | def test_compute_eng_grad(self): 75 | img = self.img4[:,:,0:3] 76 | eng = compute_eng_grad(img) 77 | self.assertLessEqual(np.max(eng), 255) 78 | self.assertGreaterEqual(np.min(eng), 0) 79 | 80 | def test_compute_eng_color(self): 81 | img = self.img4[:,:,0:3] 82 | rgb_weights = [-3, 1, -3] 83 | expected_eng = [ 84 | [-461, -1107, -909, -565, -1191], 85 | [-1036, -979, -714, -329, -151], 86 | [-301, -283, -357, -787, -975], 87 | [-416, -898, -66, -549, -675], 88 | [-562, -838, -582, -603, -760] 89 | ] 90 | eng = compute_eng_color(img, rgb_weights) 91 | self.assertGreaterEqual(eng.tolist(), expected_eng) 92 | 93 | def test_compute_eng(self): 94 | img4 = self.img4 95 | rgb_weights = [-3, 1, -3] 96 | mask_weight = 10 97 | eng = compute_eng(img4, rgb_weights, mask_weight) 98 | 99 | def test_remove_seam(self): 100 | img4 = self.img4 101 | seam = np.array([[0],[1],[2],[3],[0]]) 102 | img4_removed = remove_seam(img4, seam) 103 | 104 | self.assertEqual(img4_removed[:,:,0].tolist(), 105 | [[244, 231, 126, 249], 106 | [151, 219, 9, 64], 107 | [88, 93, 112, 155], 108 | [114, 55, 55, 205], 109 | [154, 24, 252, 63]] 110 | ) 111 | 112 | self.assertEqual(img4_removed[:,:,1].tolist(), 113 | [[228, 195, 68, 102], 114 | [92, 216, 64, 221], 115 | [218, 134, 35, 213], 116 | [229, 23, 192, 147], 117 | [218, 78, 231, 146]] 118 | ) 119 | 120 | self.assertEqual(img4_removed[:,:,2].tolist(), 121 | [[201, 137, 85, 182], 122 | [225, 91, 122, 60], 123 | [85, 46, 162, 241], 124 | [101, 252, 31, 69], 125 | [198, 196, 26, 239]] 126 | ) 127 | 128 | self.assertEqual(img4_removed[:,:,3].tolist(), 129 | [[1, 0, -1, 0], 130 | [1, 0, -1, 1], 131 | [0, 1, -1, -1], 132 | [1, 1, 0, 1], 133 | [-1, 0, 1, 0]] 134 | ) 135 | 136 | def test_add_seam(self): 137 | img4 = self.img4 138 | seam = np.array([[0],[1],[2],[3],[0]]) 139 | rgb_weights = [-3, 1, -3] 140 | mask_weight = 10 141 | eng = compute_eng(img4, rgb_weights, mask_weight) 142 | img4_added, _ = add_seam(img4, seam, eng) 143 | self.assertEqual(img4_added[:,:,0].tolist(), 144 | [[101., 172.5, 244., 231., 126., 249.], 145 | [151., 249., 206.33333333333334, 219., 9., 64.], 146 | [88., 93., 21., 75.33333333333333, 112., 155.], 147 | [114., 55., 55., 120., 126.66666666666667, 205.], 148 | [84., 119., 154., 24., 252., 63.]] 149 | ) 150 | 151 | self.assertEqual(img4_added[:,:,1].tolist(), 152 | [[ 115., 171.5, 228., 195., 68., 102.], 153 | [ 92., 74., 127.33333333333333, 216., 64., 221.], 154 | [ 218., 134., 123., 97.33333333333333, 35., 213.], 155 | [ 229., 23., 192., 111., 150., 147.], 156 | [ 164., 191., 218., 78., 231., 146.]] 157 | ) 158 | 159 | self.assertEqual(img4_added[:,:,2].tolist(), 160 | [[91., 146., 201., 137., 85., 182.], 161 | [225., 102., 139.33333333333334, 91., 122., 60.], 162 | [85., 46., 139., 115.66666666666667, 162., 241.], 163 | [101., 252., 31., 100., 66.66666666666667, 69.], 164 | [158., 178., 198., 196., 26., 239.]] 165 | ) 166 | 167 | self.assertEqual(img4_added[:,:,3].tolist(), 168 | [[-1., 0., 1., 0., -1., 0.], 169 | [1., 1., 0.6666666666666666, 0., -1., 1.], 170 | [0., 1., 0., 0., -1., -1.], 171 | [1., 1., 0., -1., 0., 1.], 172 | [-1., -1., -1., 0., 1., 0.]] 173 | ) 174 | 175 | 176 | def test_find_seams(self): 177 | eng = np.vstack(( 178 | [577.2127, -474.0578, -211.6035, -183.2227, -471.2210], 179 | [-382.3976, -653.9937, -384.5538, 12.8625, 287.8903], 180 | [66.4436, -143.5701, -3.5477, -480.0582, -578.8598], 181 | [39.5649, -722.9933, 94.4719, -420.0252, -159.3623], 182 | [309.9671, -422.5076, -206.7120, -157.1606, 111.4617] 183 | )) 184 | expected_M = [ 185 | [577.2127, -474.0578, -211.6035, -183.2227, -471.221], 186 | [-856.4554, -1128.0515, -858.6116, -458.3585, -183.33069999999998], 187 | [-1061.6079, -1271.6216, -1131.5992, -1338.6698, -1037.2183], 188 | [-1232.0566999999999, -1994.6149, -1244.1979, -1758.695, -1498.0321], 189 | [-1684.6478, -2417.1225, -2201.3269, -1915.8555999999999, -1647.2332999999999] 190 | ] 191 | expected_P = [ 192 | [-1, -1, -1, -1, -1], 193 | [1, 1, 1, 4, 4], 194 | [1, 1, 1, 2, 3], 195 | [1, 1, 3, 3, 3], 196 | [1, 1, 1, 3, 3] 197 | ] 198 | M, P = find_seams(eng) 199 | self.assertEqual(M.tolist(), expected_M) 200 | self.assertEqual(P.tolist(), expected_P) 201 | 202 | def test_get_best_seam(self): 203 | M = np.vstack(( 204 | [577.2127, -474.0578, -211.6035, -183.2227, -471.221], 205 | [-856.4554, -1128.0515, -858.6116, -458.3585, -183.33069999999998], 206 | [-1061.6079, -1271.6216, -1131.5992, -1338.6698, -1037.2183], 207 | [-1232.0566999999999, -1994.6149, -1244.1979, -1758.695, -1498.0321], 208 | [-1684.6478, -2417.1225, -2201.3269, -1915.8555999999999, -1647.2332999999999] 209 | )) 210 | P = np.vstack(( 211 | [-1, -1, -1, -1, -1], 212 | [1, 1, 1, 4, 4], 213 | [1, 1, 1, 2, 3], 214 | [1, 1, 3, 3, 3], 215 | [1, 1, 1, 3, 3] 216 | )) 217 | seam, cost = get_best_seam(M, P) 218 | self.assertEqual(seam.tolist(), [ 219 | [1], 220 | [1], 221 | [1], 222 | [1], 223 | [1] 224 | ]) 225 | self.assertEqual(cost, -2417.1225) 226 | 227 | 228 | def test_reduce_width(self): 229 | img4 = self.img4 230 | rgb_weights = [-3, 1, -3] 231 | mask_weight = 10 232 | eng = compute_eng(img4, rgb_weights, mask_weight) 233 | seam, reduced_img4, cost = reduce_width(img4, eng) 234 | self.assertEqual(reduced_img4.shape, (5, 4, 4)) 235 | 236 | 237 | def test_reduce_height(self): 238 | img4 = self.img4 239 | rgb_weights = [-3, 1, -3] 240 | mask_weight = 10 241 | eng = compute_eng(img4, rgb_weights, mask_weight) 242 | seam, reduced_img4, cost = reduce_height(img4, eng) 243 | self.assertEqual(reduced_img4.shape, (4, 5, 4)) 244 | 245 | 246 | def test_increase_width(self): 247 | img4 = self.img4 248 | rgb_weights = [-3, 1, -3] 249 | mask_weight = 10 250 | eng = compute_eng(img4, rgb_weights, mask_weight) 251 | seam, increaseded_img4, cost, updated_eng = increase_width(img4, eng) 252 | self.assertEqual(increaseded_img4.shape, (5, 6, 4)) 253 | 254 | 255 | def test_increase_height(self): 256 | img4 = self.img4 257 | rgb_weights = [-3, 1, -3] 258 | mask_weight = 10 259 | eng = compute_eng(img4, rgb_weights, mask_weight) 260 | seam, increaseded_img4, cost, updated_eng = increase_height(img4, eng) 261 | self.assertEqual(increaseded_img4.shape, (6, 5, 4)) 262 | 263 | 264 | def test_intelligent_resize(self): 265 | img = self.img4[:,:,0:3] 266 | mask = self.img4[:,:,3] 267 | rgb_weights = [-3, 1, -3] 268 | mask_weight = 10 269 | 270 | resized_img = intelligent_resize(img, 0, -1, rgb_weights, mask, mask_weight) 271 | self.assertEqual(resized_img.shape, (5, 4, 3)) 272 | 273 | resized_img = intelligent_resize(img, -1, 0, rgb_weights, mask, mask_weight) 274 | self.assertEqual(resized_img.shape, (4, 5, 3)) 275 | 276 | resized_img = intelligent_resize(img, -1, -1, rgb_weights, mask, mask_weight) 277 | self.assertEqual(resized_img.shape, (4, 4, 3)) 278 | 279 | resized_img = intelligent_resize(img, 0, 1, rgb_weights, mask, mask_weight) 280 | self.assertEqual(resized_img.shape, (5, 6, 3)) 281 | 282 | resized_img = intelligent_resize(img, 1, 0, rgb_weights, mask, mask_weight) 283 | self.assertEqual(resized_img.shape, (6, 5, 3)) 284 | 285 | resized_img = intelligent_resize(img, 1, 1, rgb_weights, mask, mask_weight) 286 | self.assertEqual(resized_img.shape, (6, 6, 3)) 287 | 288 | if __name__ == '__main__': 289 | unittest.main() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | long_description = """ 5 | seam_carver is a small tool for retargetting images to any dimension greater or smaller. 6 | It uses the process of seam carving as originally described by Shai Avidan & Ariel Shamir in http://perso.crans.org/frenoy/matlab2012/seamcarving.pdf 7 | 8 | A combination of the gradient energy (determined with the sobel filter) and a simple color energy is 9 | used to determine the least important seam. Additon of seams occurs by the same mechanism as described 10 | in the paper. 11 | """ 12 | 13 | 14 | setup(name='seam_carver', 15 | description='A small tool for retargetting images to any dimension greater or smaller', 16 | long_description=long_description, 17 | version='0.1.2', 18 | url='https://github.com/dharness/seam_carving', 19 | author='Dylan Harness', 20 | author_email='dharness.engineer@gmail.com', 21 | license='MIT', 22 | classifiers=[ 23 | 'Development Status :: 4 - Beta', 24 | 'Intended Audience :: Developers', 25 | 'Programming Language :: Python :: 3' 26 | ], 27 | keywords='seam seamcarving carving liquidresizing retarget retargetting retargeting', 28 | packages=find_packages(), 29 | install_requires=[ 30 | 'scipy>=0.19.1', 31 | 'numpy>=1.13.1', 32 | 'Pillow>=4.2.1' 33 | ], 34 | python_requires='>=3' 35 | ) --------------------------------------------------------------------------------