├── .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 | 
61 |
62 | **Resized**
63 |
64 | 
65 |
66 |
67 | **Original**
68 |
69 | 
70 |
71 | **Resized**
72 |
73 | 
74 |
75 | ### Limitations
76 |
77 | **Original**
78 |
79 | 
80 |
81 | **Resized**
82 |
83 | 
--------------------------------------------------------------------------------
/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 | )
--------------------------------------------------------------------------------