|
"""Retargets an .svg using image-domain seam carving to shrink it.""" |
|
import os |
|
import pydiffvg |
|
import argparse |
|
import torch as th |
|
import scipy.ndimage.filters as filters |
|
import numba |
|
import numpy as np |
|
import skimage.io |
|
|
|
|
|
def energy(im): |
|
"""Compute image energy. |
|
|
|
Args: |
|
im(np.ndarray) with shape [h, w, 3]: input image. |
|
|
|
Returns: |
|
(np.ndarray) with shape [h, w]: energy map. |
|
""" |
|
f_dx = np.array([ |
|
[-1, 0, 1 ], |
|
[-2, 0, 2 ], |
|
[-1, 0, 1 ], |
|
]) |
|
f_dy = f_dx.T |
|
dx = filters.convolve(im.mean(2), f_dx) |
|
dy = filters.convolve(im.mean(2), f_dy) |
|
|
|
return np.abs(dx) + np.abs(dy) |
|
|
|
|
|
@numba.jit(nopython=True) |
|
def min_seam(e): |
|
"""Finds the seam with minimal cost in an energy map. |
|
|
|
Args: |
|
e(np.ndarray) with shape [h, w]: energy map. |
|
|
|
Returns: |
|
min_e(np.ndarray) with shape [h, w]: for all (y,x) min_e[y, x] |
|
is the cost of the minimal seam from 0 to y (top to bottom). |
|
The minimal seam can be found by looking at the last row of min_e. |
|
This is computed by dynamic programming. |
|
argmin_e(np.ndarray) with shape [h, w]: for all (y,x) argmin_e[y, x] |
|
contains the x coordinate corresponding to this seam in the |
|
previous row (y-1). We use this for backtracking. |
|
""" |
|
|
|
min_e = e.copy() |
|
argmin_e = np.zeros_like(e, dtype=np.int64) |
|
|
|
h, w = e.shape |
|
|
|
|
|
for y in range(1, h): |
|
for x in range(w): |
|
if x == 0: |
|
idx = np.argmin(e[y-1, x:x+2]) |
|
argmin_e[y, x] = idx + x |
|
mini = e[y-1, x + idx] |
|
elif x == w-1: |
|
idx = np.argmin(e[y-1, x-1:x+1]) |
|
argmin_e[y, x] = idx + x - 1 |
|
mini = e[y-1, x + idx - 1] |
|
else: |
|
idx = np.argmin(e[y-1, x-1:x+2]) |
|
argmin_e[y, x] = idx + x - 1 |
|
mini = e[y-1, x + idx - 1] |
|
|
|
min_e[y, x] = min_e[y, x] + mini |
|
|
|
return min_e, argmin_e |
|
|
|
|
|
def carve_seam(im): |
|
"""Carves a vertical seam in an image, reducing it's horizontal size by 1. |
|
|
|
Args: |
|
im(np.ndarray) with shape [h, w, 3]: input image. |
|
|
|
Returns: |
|
(np.ndarray) with shape [h, w-1, 1]: the image with one seam removed. |
|
""" |
|
|
|
e = energy(im) |
|
min_e, argmin_e = min_seam(e) |
|
h, w = im.shape[:2] |
|
|
|
|
|
to_keep = np.ones((h, w), dtype=np.bool) |
|
|
|
|
|
x = np.argmin(min_e[-1]) |
|
print("carving seam", x, "with energy", min_e[-1, x]) |
|
|
|
|
|
for y in range(h-1, -1, -1): |
|
|
|
to_keep[y, x] = False |
|
x = argmin_e[y, x] |
|
|
|
|
|
to_keep = np.stack(3*[to_keep], axis=2) |
|
new_im = im[to_keep].reshape((h, w-1, 3)) |
|
return new_im |
|
|
|
|
|
def render(canvas_width, canvas_height, shapes, shape_groups, samples=2): |
|
_render = pydiffvg.RenderFunction.apply |
|
scene_args = pydiffvg.RenderFunction.serialize_scene(\ |
|
canvas_width, canvas_height, shapes, shape_groups) |
|
|
|
img = _render(canvas_width, |
|
canvas_height, |
|
samples, |
|
samples, |
|
0, |
|
None, |
|
*scene_args) |
|
return img |
|
|
|
|
|
def vector_rescale(shapes, scale_x=1.00, scale_y=1.00): |
|
new_shapes = [] |
|
for path in shapes: |
|
path.points[..., 0] *= scale_x |
|
path.points[..., 1] *= scale_y |
|
|
|
|
|
def main(): |
|
parser = argparse.ArgumentParser() |
|
parser.add_argument("--svg", default=os.path.join("imgs", "hokusai.svg")) |
|
parser.add_argument("--optim_steps", default=10, type=int) |
|
parser.add_argument("--lr", default=1e-1, type=int) |
|
args = parser.parse_args() |
|
|
|
name = os.path.splitext(os.path.basename(args.svg))[0] |
|
root = os.path.join("results", "seam_carving", name) |
|
svg_root = os.path.join(root, "svg") |
|
os.makedirs(root, exist_ok=True) |
|
os.makedirs(os.path.join(root, "svg"), exist_ok=True) |
|
|
|
pydiffvg.set_use_gpu(False) |
|
|
|
|
|
|
|
print("loading svg %s" % args.svg) |
|
canvas_width, canvas_height, shapes, shape_groups = \ |
|
pydiffvg.svg_to_scene(args.svg) |
|
print("done loading") |
|
|
|
max_size = 512 |
|
scale_factor = max_size / max(canvas_width, canvas_height) |
|
print("rescaling from %dx%d with scale %f" % (canvas_width, canvas_height, scale_factor)) |
|
canvas_width = int(canvas_width*scale_factor) |
|
canvas_height = int(canvas_height*scale_factor) |
|
print("new shape %dx%d" % (canvas_width, canvas_height)) |
|
vector_rescale(shapes, scale_x=scale_factor, scale_y=scale_factor) |
|
|
|
|
|
|
|
num_seams_to_remove = canvas_width // 3 |
|
new_canvas_width = canvas_width - num_seams_to_remove |
|
scaling = new_canvas_width * 1.0 / canvas_width |
|
|
|
|
|
print("rendering naive rescaling...") |
|
vector_rescale(shapes, scale_x=scaling) |
|
resized = render(new_canvas_width, canvas_height, shapes, shape_groups) |
|
pydiffvg.imwrite(resized.cpu(), os.path.join(root, 'uniform_scaling.png'), gamma=2.2) |
|
pydiffvg.save_svg(os.path.join(svg_root, 'uniform_scaling.svg') , canvas_width, |
|
canvas_height, shapes, shape_groups, use_gamma=False) |
|
vector_rescale(shapes, scale_x=1.0/scaling) |
|
print("saved naiving scaling") |
|
|
|
|
|
print("rendering initial state...") |
|
im = render(canvas_width, canvas_height, shapes, shape_groups) |
|
pydiffvg.imwrite(im.cpu(), os.path.join(root, 'init.png'), gamma=2.2) |
|
pydiffvg.save_svg(os.path.join(svg_root, 'init.svg'), canvas_width, |
|
canvas_height, shapes, shape_groups, use_gamma=False) |
|
print("saved initial state") |
|
|
|
|
|
|
|
|
|
retargeted = im[..., :3].cpu().numpy() |
|
previous_width = canvas_width |
|
print("carving seams") |
|
for seam_idx in range(num_seams_to_remove): |
|
print('\nseam', seam_idx+1, 'of', num_seams_to_remove) |
|
|
|
|
|
retargeted = carve_seam(retargeted) |
|
|
|
current_width = canvas_width - seam_idx - 1 |
|
scale_factor = current_width * 1.0 / previous_width |
|
previous_width = current_width |
|
|
|
padded = np.zeros((canvas_height, canvas_width, 4)) |
|
padded[:, :-seam_idx-1, :3] = retargeted |
|
padded[:, :-seam_idx-1, -1] = 1.0 |
|
padded = th.from_numpy(padded).to(im.device) |
|
|
|
|
|
|
|
points_vars = [] |
|
|
|
mini, maxi = canvas_width, 0 |
|
for path in shapes: |
|
path.points.requires_grad = False |
|
x = path.points[..., 0] |
|
y = path.points[..., 1] |
|
|
|
|
|
x = x * scale_factor |
|
|
|
|
|
path.points[..., 0] = th.clamp(x, 0, current_width) |
|
path.points[..., 1] = th.clamp(y, 0, canvas_height) |
|
|
|
path.points.requires_grad = True |
|
points_vars.append(path.points) |
|
path.stroke_width.requires_grad = True |
|
|
|
|
|
mini = min(mini, path.points.min().item()) |
|
maxi = max(maxi, path.points.max().item()) |
|
print("points", mini, maxi, "scale", scale_factor) |
|
|
|
|
|
|
|
geom_optim = th.optim.Adam(points_vars, lr=args.lr) |
|
|
|
for step in range(args.optim_steps): |
|
geom_optim.zero_grad() |
|
|
|
img = render(canvas_width, canvas_height, shapes, shape_groups, |
|
samples=2) |
|
|
|
pydiffvg.imwrite( |
|
img.cpu(), |
|
os.path.join(root, "seam_%03d_iter_%02d.png" % (seam_idx, step)), gamma=2.2) |
|
|
|
|
|
loss = (img - padded)[..., :3].pow(2).mean() |
|
|
|
print('render loss:', loss.item()) |
|
|
|
|
|
loss.backward() |
|
|
|
|
|
geom_optim.step() |
|
pydiffvg.save_svg(os.path.join(svg_root, "seam%03d.svg" % seam_idx), |
|
canvas_width-seam_idx, canvas_height, shapes, |
|
shape_groups, use_gamma=False) |
|
|
|
for path in shapes: |
|
mini = min(mini, path.points.min().item()) |
|
maxi = max(maxi, path.points.max().item()) |
|
print("points", mini, maxi) |
|
|
|
img = render(canvas_width, canvas_height, shapes, shape_groups) |
|
img = img[:, :-num_seams_to_remove] |
|
|
|
pydiffvg.imwrite(img.cpu(), os.path.join(root, 'final.png'), |
|
gamma=2.2) |
|
pydiffvg.imwrite(retargeted, os.path.join(root, 'ref.png'), |
|
gamma=2.2) |
|
|
|
pydiffvg.save_svg(os.path.join(svg_root, 'final.svg'), |
|
canvas_width-seam_idx, canvas_height, shapes, |
|
shape_groups, use_gamma=False) |
|
|
|
|
|
from subprocess import call |
|
call(["ffmpeg", "-framerate", "24", "-i", os.path.join(root, "seam_%03d_iter_00.png"), "-vb", "20M", |
|
os.path.join(root, "out.mp4")]) |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |
|
|