Seam carving--让图片比例随心缩放

·  阅读 2034

介绍

大哥,帮我P个图呗
忙着呢,没空
很简单的,你看就是这个图他宽了点,放到页面上人都挤扁了,帮稍微P窄一点点就好了
那你把图片裁掉一点不就行了
裁掉人就看不全了
好吧,发我邮箱有空帮你弄下
复制代码

为了更好的显示效果,我们经常需要调整一下图片尺寸,而PS技术又不熟练,看完这篇文章,你就不用求着设计师帮你P图了

Seam carving算法是一种有趣的图像缩放算法,和常见的外框裁剪或者几何拉伸不同,算法能感知到图片内容,区分出主要的物体,避开这些主体的基础上进行变形的,例如:

上面,1是原图,2是Seam-carving算法缩放后的图片,3是经过简单压缩尺寸得到的图片,4是按目标尺寸裁剪后的图片。

可以看到图2虽然宽度缩小了,但是其中的人和建筑并没有明显的变形,而3中的人和建筑已经被压扁了,4中建筑已经不完整了。人和建筑作为图片中主要元素,被算法感知到,并在处理时被尽量的保留了下来。

当然,算法也可以应用到垂直方向上,或者同时应用到水平和垂直两个方向,例如:

甚至,可以将图片中某个物体标记出来,定向的移除这个物体,效果几乎可以媲美专业的PS。找找看,下面哪只鞋子不见了

如果没找到,没关系,文章最后提供了参考答案
复制代码

原理

背景介绍

2007年,Shai Avidan和Ariel Shamir发表的《Seam Carving for Content-Aware Image Resizing》中首次提出Seam carving算法,文中提到,图片的排版布局形式多样,同一张图片往往需要很多不同的尺寸来适应不同的场景和设备,而仅仅是外框裁剪亦或是简单的几何缩放效果都不是很好,所以需要一种优雅的方式来动态的调整图片的尺寸,而这种尺寸的变换又需要能很好的保留图片想要表达的信息,这就是Seam carving算法。

理论

为了使变换结果尽可能的自然,需要找出图片中不重要的,包含信息量少的像素,论文中用的描述是unnoticeable pixels that blend with their surroundings。 这里定义了一个能量函数的概念,文中给出了能量函数如下:

e_1(I) = \left|\frac{\partial}{\partial x}I\right| + \left|\frac{\partial}{\partial y}I\right|

能量越大,意味着包含的信息越多,对图片进行变换时需要尽可能避开能量大的像素

算法的逻辑是这样的,假设需要将图片的宽度从600裁剪到400。先找出一条竖直方向上的夹缝,从图片中移除这条夹缝,宽度从600减为599,重复200次,即可到一张宽度400的新图。

上面提到的一条竖直方向上的夹缝,有如下要求,

  • 在图片每行有且仅有一个像素点
  • 相邻的两行之间的像素坐标\left|\Delta x\right| \leq 1.

为了找到一条最小能量的夹缝(Seam),从最上方第一行开始,向下列出所有可能的夹缝路线,最后找到能量损失最小的那条,其中位于(i, j)坐标处的能量损失定义如下:

M(i, j) = e(i, j) + min(M(i -1 , j - 1), M(i - 1, j), M(i - 1, j + 1))

找出夹缝后,将夹缝移除,图片宽度-=1;
然后继续计算新的图片中的能量和能量损失,找出下一条损失最小的夹缝,一直减少图片宽度,直到图片宽度满足要求

示例

下面展示下裁剪图片的具体流程:

  • 下方图1为待处理图片
  • 根据e_1(I)计算能量,得到图2
  • 从上往下计算能量损失,得到结果如图3所示
  • 找到一条能量损失最小的夹缝,夹缝路径如图4中红线所示
  • 移除夹缝后,回到步骤2,继续寻找下一条夹缝

原图尺寸为640x434,为了将宽度缩小至400,也就是我们需要移除240条竖直方向上的夹缝,下图是所有移除的240条夹缝在原图中的位置。

拉伸

与裁剪类似,图片拉伸也是在能量最小的像素上对图片进行操作,找出能量损失最小的夹缝,将夹缝复制插入原先位置,循环往复,即可实现图片的拉伸。
需要注意的是,与裁剪操作每次循环操作一条夹缝不同,拉伸操作需要一次性找出所有待插入的夹缝(取能量损失从小到大前size条),批量整体插入夹缝,否则每次循环将会找到同一条夹缝,并不断的重复插入。

物体移除

为了定向的移除图片中的物体,只需在energy_function计算后将物体对应的像素的能量值手动调小,这样一来找到的夹缝自然会经过定向的物体,循环数次之后,待移除的物体就从图片中被移除了。这时再使用拉伸操作将图片拉回原始尺寸,物体的移除操作就完成了

能量函数

这里还有一个问题,能量函数为什么是梯度的L_1距离。
贴心的作者在文中给出了解释:

We have tested both L_1 and L_{2}\!-\!norm of the gradient, saliency measure [Itti et al. 1999], and Harris-corners measure [Harris and Stephens 1988]. We also used eye gaze measurement [DeCarlo and Santella 2002], and the output of face detectors.

在比对了一番后,得出结论,没有哪一个能适合所有场景,但是总的来说,e_1e_{HoG} 这两个表现的不错,其中e_{HoG}定义如下:

e_{HoG}(I) = \frac{\left|\frac{\partial}{\partial x}I\right| + \left|\frac{\partial}{\partial y}I\right|}{max(HoG(I(x,y))}

3、实现

优化

因为每一条夹缝都需要重新计算能量和能量损失,这个算法的计算量还是比较大的,上面图片从640裁剪到400,在我的笔记本上平均执行6s左右。
考虑到夹缝移除后,其他部分的能量是不会变化的,其实仅需更新移除附近的能量值,按照这个思路,优化后大概减伤了0.5s的计算时间,但是整个计算时间还是偏长。

因为平时java用的比较顺手,就用java重新撸了一遍,处理同一张图片时间减少到了1s左右,效率上也不是很理想,希望后面能想办法再优化一下效率。

代码

最后,附上完整的python代码,其中:
reduce(image, size)方法提供了图片的裁剪
enlarge(image, size)方法提供了图片的拉伸
remove_object(image, mask)方法提供了物体移除
energy_function(image)实现的是e_1(I),有兴趣的朋友可以尝试下其他能量函数

python 3.7.3

import numpy as np
import matplotlib.pyplot as plt
from skimage import color, io, util
from time import time

def energy_function(image):
    gray_image = color.rgb2gray(image)
    gradient = np.gradient(gray_image)
    return np.absolute(gradient[0]) + np.absolute(gradient[1])

def compute_cost(image, energy, axis=1):
    energy = energy.copy()

    if axis == 0:
        energy = np.transpose(energy, (1, 0))

    H, W = energy.shape

    cost = np.zeros((H, W))
    paths = np.zeros((H, W), dtype=np.int)

    # Initialization
    cost[0] = energy[0]
    paths[0] = 0

    for row in range(1, H):
        upL = np.insert(cost[row - 1, 0:W - 1], 0, 1e10, axis=0)
        upM = cost[row - 1, :]
        upR = np.insert(cost[row - 1, 1:W], W - 1, 1e10, axis=0)
        upchoices = np.concatenate((upL, upM, upR), axis=0).reshape(3, -1)

        # M(i, j) = e(i, j) + min(M(i -1 , j - 1), M(i - 1, j), M(i - 1, j + 1))
        cost[row] = energy[row] + np.min(upchoices, axis=0)

        # left = -1
        # middle = 0
        # right = 1
        paths[row] = np.argmin(upchoices, axis=0) - 1

    if axis == 0:
        cost = np.transpose(cost, (1, 0))
        paths = np.transpose(paths, (1, 0))

    return cost, paths

def backtrack_seam(paths, end):
    H, W = paths.shape
    seam = - np.ones(H, dtype=np.int)

    seam[H - 1] = end

    for h in range(H - 1, 0, -1):
        seam[h - 1] = seam[h] + paths[h, end]
        end += paths[h, end]

    return seam

def remove_seam(image, seam):
    if len(image.shape) == 2:
        image = np.expand_dims(image, axis=2)

    H, W, C = image.shape

    mask = np.ones_like(image, bool)
    for h in range(H):
        mask[h, seam[h]] = False
    out = image[mask].reshape(H, W - 1, C)
    out = np.squeeze(out)

    return out

def reduce(image, size, axis=1, efunc=energy_function, cfunc=compute_cost):
    out = np.copy(image)
    if axis == 0:
        out = np.transpose(out, (1, 0, 2))

    while out.shape[1] > size:
        energy = efunc(out)
        costs, paths = cfunc(out, energy)
        end = np.argmin(costs[-1])
        seam = backtrack_seam(paths, end)
        out = remove_seam(out, seam)

    if axis == 0:
        out = np.transpose(out, (1, 0, 2))

    return out

def duplicate_seam(image, seam):
    if len(image.shape) == 2:
        image = np.expand_dims(image, axis=2)

    H, W, C = image.shape
    out = np.zeros((H, W + 1, C))

    for h in range(H):
        out[h] = np.vstack((image[h, :seam[h]], image[h, seam[h]], image[h, seam[h]:]))

    return out

def find_seams(image, k, axis=1, efunc=energy_function, cfunc=compute_cost):
    image = np.copy(image)
    if axis == 0:
        image = np.transpose(image, (1, 0, 2))

    H, W, C = image.shape
    indices = np.tile(range(W), (H, 1))
    seams = np.zeros((H, W), dtype=np.int)

    for i in range(k):
        # Get the current optimal seam
        energy = efunc(image)
        cost, paths = cfunc(image, energy)
        end = np.argmin(cost[H - 1])
        seam = backtrack_seam(paths, end)

        # Remove that seam from the image
        image = remove_seam(image, seam)

        # Store the new seam with value i+1 in the image
        seams[np.arange(H), indices[np.arange(H), seam]] = i + 1

        # Remove the indices used by the seam, so that `indices` keep the same shape as `image`
        indices = remove_seam(indices, seam)

    if axis == 0:
        seams = np.transpose(seams, (1, 0))

    return seams

def enlarge(image, size, axis=1, efunc=energy_function, cfunc=compute_cost):
    out = np.copy(image)
    if axis == 0:
        out = np.transpose(out, (1, 0, 2))

    H, W, C = out.shape

    seams = find_seams(out, size - W)
    for i in range(size - W):
        seam = np.where(seams == i + 1)[1]
        out = duplicate_seam(out, seam)

    if axis == 0:
        out = np.transpose(out, (1, 0, 2))

    return out

def remove_object(image, mask):
    assert image.shape[:2] == mask.shape

    H, W, _ = image.shape
    out = np.copy(image)

    H,W,C = out.shape
    while not np.all(mask == 0):
        energy = energy_function(out)
        weighted_energy = energy + mask * (-100)
        cost, paths = compute_cost(out, weighted_energy)
        end = np.argmin(cost[-1])

        seam = backtrack_seam(paths, end)
        out = remove_seam(out, seam)
        mask = remove_seam(mask,seam)

    return enlarge(out, W, axis=1)


tower = io.imread('imgs/tower_original.jpg')
tower = util.img_as_float(tower)
plt.subplot(1, 2, 1)
plt.imshow(tower)

out = reduce(tower, 400)
plt.subplot(1, 2, 2)
plt.imshow(out)

plt.show()
复制代码

参考

最后参考答案,左上角的是原图,其他三张图分别移除了一只鞋子

分类:
阅读
标签:
分类:
阅读
标签: