Seam Carving算法是一种图像缩放算法,它能够将图像缩放也不影响图片的主要内容。它主要分为:
一、计算图像的能量图
二、通过能量图计算代价图以及路径图
三、通过代价图以及路径图寻找能量最低的seam
四、重复上述步骤
一、能量图的计算
为了定义图像中像素的重要程度,我们计算图像中像素的“能量”。“能量”是用来衡量像素的重要程度。我们知道如果一个像素具有较大的梯度,说明这个像素很大可能为边缘。而边缘往往是一副图像的重要内容。我们定义以下能量函数去计算每个像素的能量:
E
(
I
)
=
∣
∂
∂
x
I
∣
+
∣
∂
∂
y
I
∣
E(I)=|\frac{∂}{∂x}I|+|\frac{∂}{∂y}I|
E(I)=∣∂x∂I∣+∣∂y∂I∣
这里我们遍历图像中每个像素,计算其能量,输出一个与图像相同尺寸的能量图。这里我们利用np.gradient
直接计算梯度,进而计算能量。计算能量图的函数如下:
from skimage import color
from skimage import color,io,measure
import numpy as np
def energy_function(image):
image_gray = image.copy()
image_gray = color.rgb2gray(image_gray)
H,W = image.shape
G = np.gradient(image)
out = np.abs(G[0])+np.abs(G[1])
return out
查看效果:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
from skimage import color
from IPython.display import HTML
from time import time
plt.rcParams['figure.figsize']=(15,12)
plt.rcParams['image.interpolation']='nearest'
plt.rcParams['image.cmap']='gray'
fig = plt.gcf()
fig.set_size_inches(18.5,13.5)
energy_map = energy_function(image)
plt.title('Energy_map')
plt.subplot(211)
plt.imshow(image)
plt.subplot(212)
plt.imshow(energy_map)
plt.show()
输出:
我们发现energy_map
中边缘像素具有较大亮度,且能量图像也能显现图像的内容。
二、计算代价和路径图
为了说明计算代价的意义,我们先解释何为seam。seam定义为像素从上到下(或从左到右)的连接路径。对于从上到下的像素,我们将从每一行中选取一个像素。这些连接的路径就组成了一条seam。seam的数学定义为:
s
x
=
{
s
i
x
}
i
=
1
n
=
{
x
(
i
)
,
i
}
i
=
1
n
,
s
.
t
.
,
∀
∣
x
(
i
)
−
x
(
i
−
1
)
∣
≤
1
s^x=\{s_i^x\}_{i=1}^n=\{x(i),i\}_{i=1}^n,s.t.,\forall |x(i)-x(i-1)|≤1
sx={six}i=1n={x(i),i}i=1n,s.t.,∀∣x(i)−x(i−1)∣≤1
s
y
=
{
s
i
y
}
j
=
1
m
=
{
j
,
y
(
j
)
}
j
=
1
n
,
s
.
t
.
,
∀
∣
y
(
j
)
−
y
(
j
−
1
)
∣
≤
1
s^y=\{s_i^y\}_{j=1}^m=\{j,y(j)\}_{j=1}^n,s.t.,\forall |y(j)-y(j-1)|≤1
sy={siy}j=1m={j,y(j)}j=1n,s.t.,∀∣y(j)−y(j−1)∣≤1
第一个式子为从从上到下seam,第二个式子为从左到右的seam。那么seam对于图像缩放有何意义呢?例如,对于200×200的图像,我们删除一条从上到下的seam,那么图像就变为200×199的尺寸。所以如果删除一些seam,就可以达到缩放的目的。但是我们删除什么样的seam呢?我们知道,能量定义了一个像素在图像中的重要程度。为了保留图像中的主要内容,我们删除图像中图像中总能量较低的seam,这样就保留了图像中的主要内容。
那么如何计算代价呢?我们知道对于从上到下的seam。如果一个像素的横坐标为x,那么上一行组成seam的像素的横坐标只能是x-1,x,x+1。那么我们为了让能量最小,我们能选择上一行这三个像素中能量最小的像素和该像素组成seam,那么通过该点时的总能量为这一点的能量加上上一行像素总能量的最小值。所以可以利用以下递推公式(从上到下)计算代价:
M
(
i
,
j
)
=
E
(
i
,
j
)
+
m
i
n
(
M
(
i
−
1
,
j
−
1
)
,
M
(
i
−
1
,
j
)
,
M
(
i
−
1
,
j
+
1
)
)
M(i,j)=E(i,j)+min(M(i-1,j-1),M(i-1,j),M(i-1,j+1))
M(i,j)=E(i,j)+min(M(i−1,j−1),M(i−1,j),M(i−1,j+1))
例如:
能量图为:
代价的计算方式为:
由于计算的最后一行时,为经过最后一行像素的seam的最小总能量。那么我们只要挑选总能量最小的像素,并进行回溯,找到这个最小能量对应的seam即可。计算代价的函数如下:
def compute_cost(image,energy_map,axis=1):
if axis==0:
energy_map = np.transpose(energy_map)
H,W = energy_map.shape
Energy_cost = np.zeros((H,W))
path = np.zeros((H,W))
Energy_cost[0,:] = energy_map[0,:].copy()
for i in range(1,H):
for j in range(1,W):
Cost_list = np.array([Energy_cost[i-1,j-1],Energy_cost[i-1,j],\
Energy_cost[i-1,min(j+1,W-1)]])
index = np.argmin(Cost_list)
Energy_cost[i,j] = energy_map[i,j] + Cost_list[index]
path[i,j] = index-1
Cost_list_left = np.array([Energy_cost[i-1,0],Energy_cost[i-1,1]])
index = np.argmin(Cost_list_left)
Energy_cost[i,0] = Cost_list_left[index] + energy_map[i,0]
path[i,0] = index
if axis == 0:
path = np.transpose(path)
Energy_cost = np.transpose(Energy_cost)
return path,Energy_cost
注意这里的path
用于记录路径,便于回溯时找到seam。对于path[i,j]
,如果(i,j)位置的能量来自上一行左边则为-1,中间为0,右边为1。还需注意的是,如果我们要寻找从左到右的seam,只需将图像翻转90°,寻找从上到下的seam。下面检测效果:
vpath,vcost = compute_cost(image,energy_map,axis=1)
plt.title('Vertical cost map')
plt.imshow(vcost,cmap='inferno')
plt.axis('off')
plt.show()
hpath,hcost =compute_cost(image,energy_map,axis=0)
plt.title('Horizonal cost map')
plt.imshow(hcost,cmap='inferno')
plt.axis('off')
plt.show()
输出为:
下面我们回溯找到相应的能量最小seam,我们只需找到最后一行总能量最小的像素,向上找到组成seam的相应的像素,如果path[i,j]=-1
,则找到左上角的像素;如果为path[i,j]=-1
则回溯至上方像素;path[i,j]=1
,回溯至右上方像素。回溯至上一行计算以上操作,直至到达图像第一行。回溯函数为:
def backtrack_seam(path,end_index):
H,W = path.shape
seam = -np.ones(H)
seam[-1] = end_index.copy()
for i in range(H-1):
if path[H-1-i,end_index] ==-1:
end_index = end_index-1
seam[H-2-i]=end_index
elif path[H-1-i,end_index] == 0:
end_index = end_index
seam[H-2-i] = end_index
else:
end_index = end_index+1
seam[H-2-i] = end_index
return seam
下面我们利用compute_cost
和backtrack_seam
计算代价并回溯找到能量最小的seam:
vpath,vcost = compute_cost(image,energy_map)
end = np.argmin(vcost[-1])
seam_energy = vcost[-1,end]
seam = backtrack_seam(vpath,end)
# Visualize seam
vseam = np.copy(image)
for row in range(vseam.shape[0]):
vseam[row, int(seam[row]), :] = np.array([1.0, 0, 0])
plt.title('Vertical Seam')
plt.axis('off')
plt.imshow(vseam)
plt.show()
输出为:
明显可以看出上图标出的seam似乎并没有经过云彩城堡或是小路。
我们得到了能量最小的seam,下面我们需要把seam给移出图片:
def remove_seam(image,seam):
seam = seam.astype(int)
if len(image.shape)==2:
image = np.expand_dims(image,axis=2)
H,W,C = image.shape
image_new = np.zeros((H,W-1,C))
for i in range(H):
image_new[i,:seam[i]]=image[i,:seam[i]]
image_new[i,seam[i]:] = image[i,seam[i]+1:]
image_new = np.squeeze(image_new)
return image_new
四、迭代的寻找seam,并移除
有了上面这些步骤,我们就可以指定图片的尺寸,对图片进行缩小,这里的主要思路是移除一条seam后在移除的图像上找到另一条seam,然后在移除;重复上述操作,直至图像到达指定的维度:
def reduce(image,size,axis=1,efunc=energy_function,cfunc=compute_cost,bfunc=\
backtrack_seam,rfunc=remove_seam):
if axis == 0:
image = np.transpose(image,(1,0,2))
image_new = image.copy()
W = image.shape[1]
image_gray = color.rgb2gray(image_new)
energy_map = efunc(image_gray)
paths,cost = cfunc(image_gray,energy_map,1)
index = np.argmin(cost[-1])
seam = bfunc(paths,index)
for i in range(W-size):
image_new = rfunc(image_new,seam)
image_gray = color.rgb2gray(image_new)
energy_map = efunc(image_gray)
paths,cost =cfunc(image_gray,energy_map,1)
index = np.argmin(cost[-1])
seam = bfunc(paths,index)
if axis ==0:
image_new = np.transpose(image_new,(1,0,2))
return image_new
这里的axis
指定我们希望缩小的是图像的维度,即宽度或是长度,axis=0
默认缩小宽度;size
是我们想要将某一维度缩小至的数值。这里的efunc
、cfunc
、bfunc
、rfunc
分别指定计算能量,代价,回溯以及移除seam的方法,这里默认为我们上面所编写的enenrgy_function
、compute_cost
、backtrack_seam
以及remove_seam
。下面查看效果:
from carving_seam import reduce
# Reduce image width
W_new = 400
out = reduce(image, W_new)
plt.subplot(2, 1, 1)
plt.title('Original')
plt.imshow(image)
plt.subplot(2, 1, 2)
plt.title('Resized')
plt.imshow(out)
plt.show()
# Reduce image height
H_new = 300
out = reduce(image, H_new, axis=0)
plt.subplot(1, 2, 1)
plt.title('Original')
plt.imshow(image)
plt.subplot(1, 2, 2)
plt.title('Resized')
plt.imshow(out)
plt.show()
输出为:
我们可以看到实际上缩小宽度时,效果并不理想。这主要是因为我们指定的宽度实际上小,即需要找出的seam过多。
附:
一、用Seam Carving对图像进行放大处理
我们既然可以用Seam Carving找出几条能量最小的seam然后缩小图像,那么同样我们可以找出几条能量最小的seam,对图像进行放大,比如可以找出能量最小的seam,然后在相应位置重复复制这条seam:
def duplicate_seam(image,seam):
if len(image.shape)==2:
image=np.expand_dims(image,axis=2)
H,W,C = image.shape
image_new = np.zeros((H,W+1,C))
for i in range(H):
image_new[i]=np.insert(image[i],int(seam[i]),image[i,int(seam[i])],axis=0)
image_new = np.squeeze(image_new)
return image_new
def enlarge_naive(image,size,axis=1,efunc=energy_function,cfunc=compute_cost,\
bfunc = backtrack_seam,dfunc=duplicate_seam):
if axis==0:
image = np.transpose(image,(1,0,2))
W = image.shape[1]
image_gray = color.rgb2gray(image)
energy_map = efunc(image_gray)
paths,cost = cfunc(image,energy_map,1)
end_index = np.argmin(cost[-1])
seam = bfunc(paths,end_index)
image_new = image.copy()
for i in range(size-W):
image_new = dfunc(image_new,seam)
return image_new
这里的duplicate_seam
是对seam进行复制的函数,然而这样的方法虽然速度较快,但是效果并不好:
from carving_seam import enlarge_naive
W_new = 800
enlarged = enlarge_naive(image, W_new)
W = image.shape[1]
plt.subplot(211)
plt.imshow(enlarged)
plt.subplot(212)
plt.imshow(image)
plt.show()
得到:
更为成熟的想法是:找出N条能量最小的seam,进行复制:
def find_seam(image,k,efunc=energy_function,cfunc=compute_cost,bfunc=\
backtrack_seam,rfunc=remove_seam):
image_gray = color.rgb2gray(image)
energy_map=efunc(image_gray)
paths,cost=cfunc(image,energy_map,axis=1)
end_index = np.argmin(cost[-1])
seam = bfunc(paths,end_index)
seam_list=[seam]
for i in range(int(k-1)):
energy_map = rfunc(energy_map,seam)
image_gray = rfunc(image_gray,seam)
paths,cost = cfunc(image_gray,energy_map)
end_index = np.argmin(cost[-1])
seam = bfunc(paths,end_index)
seam_list.append(seam)
return np.array(seam_list)
def enlarge(image,size,axis=1,efunc=energy_function,cfunc=compute_cost,bfunc=\
backtrack_seam,rfunc=remove_seam,dfunc=duplicate_seam):
if axis ==0:
image = np.transpose(image,(1,0,2))
W = image.shape[1]
seams = find_seam(image,size-W,efunc,cfunc,bfunc,rfunc)
image_large = image.copy()
for i in range(size-W):
image_large = dfunc(image_large,seams[i])
if axis==0:
image_large = np.transpose(image_large,(1,0,2))
return image_large
这里的find_seam
为寻找能量最小的几条seam的函数,输出的每一行为相应的seam,我们可以看看他的效果:
from carving_seam import find_seam
# Alternatively, find k seams for removal and duplicate them.
W_new = 800
start = time()
W = image.shape[1]
seams = find_seam(image, W_new - W)
end = time()
print('Time of finding seams is '+str(end-start))
seam_map = image.copy()
H_S,W_S = seams.shape
for seam in seams:
for i in range(W_S):
seam_map[i,int(seam[i])] = np.array([1,0,0])
plt.imshow(seam_map)
plt.show()
输出为:
查看放大效果:
from carving_seam import enlarge
W_new = 800
W = image.shape[1]
start = time()
out = enlarge(image, W_new)
end = time()
# Can take around 20 seconds
print("Enlarging width from %d to %d: %f seconds." \
% (W, W_new, end - start))
plt.subplot(2, 1, 1)
plt.title('Original')
plt.imshow(image)
plt.subplot(2, 1, 2)
plt.title('Resized')
plt.imshow(out)
plt.show()
输出为:
我们可以在令一张图片上查看缩小和放大的效果:
###Enlarge another image
# Load image
img2 = io.imread('wave.jpg')
img2 = util.img_as_float(img2)
plt.subplot(311)
plt.title('Original Image')
plt.imshow(img2)
out = reduce(img2, 300)
plt.subplot(312)
plt.title('Reduced')
plt.imshow(out)
out = enlarge(img2, 800)
plt.subplot(313)
plt.title('Enlarged')
plt.imshow(out)
plt.show()
输出为:
二、Forward Energy
当我们使用Seam_Carving算法时,我们移除最低的能量像素并保留最高的能量像素。因此,平均图像能量增加,可能导致伪影和锯齿边缘。这里的解决方法为:考虑由于移除seam后,产生的新的邻接像素之间的差。我们在原递推公式加上约束项,让邻接像素的差尽可能小,这样我们就可以尽可能消除伪影和锯齿边缘。如图所示:
这样递推公式为:
M
(
i
,
j
)
=
E
(
i
,
j
)
+
m
i
n
(
M
(
i
−
1
,
j
−
1
)
+
C
L
(
i
,
j
)
,
M
(
i
−
1
,
j
)
+
C
U
(
i
,
j
)
,
M
(
i
−
1
,
j
+
1
)
+
C
R
(
i
,
j
)
)
M(i,j)=E(i,j)+min(M(i-1,j-1)+C_L(i,j),M(i-1,j)+C_U(i,j),M(i-1,j+1)+C_R(i,j))
M(i,j)=E(i,j)+min(M(i−1,j−1)+CL(i,j),M(i−1,j)+CU(i,j),M(i−1,j+1)+CR(i,j))
其中:
C
L
(
i
,
j
)
=
∣
I
(
i
−
1
,
j
)
−
I
(
i
,
j
−
1
)
∣
+
∣
I
(
i
,
j
−
1
)
−
I
(
i
,
j
+
1
)
∣
C_L(i,j)=|I(i-1,j)-I(i,j-1)|+|I(i,j-1)-I(i,j+1)|
CL(i,j)=∣I(i−1,j)−I(i,j−1)∣+∣I(i,j−1)−I(i,j+1)∣
C
U
(
i
,
j
)
=
∣
I
(
i
,
j
−
1
)
−
I
(
i
,
j
+
1
)
∣
C_U(i,j)=|I(i,j-1)-I(i,j+1)|
CU(i,j)=∣I(i,j−1)−I(i,j+1)∣
C
R
(
i
,
j
)
=
∣
I
(
i
,
j
−
1
)
−
i
(
i
,
j
+
1
)
∣
+
∣
I
(
i
,
j
+
1
)
−
I
(
i
−
1
,
j
)
∣
C_R(i,j)=|I(i,j-1)-i(i,j+1)|+|I(i,j+1)-I(i-1,j)|
CR(i,j)=∣I(i,j−1)−i(i,j+1)∣+∣I(i,j+1)−I(i−1,j)∣
实际上上面的三个式子就是三种情况下由于移除seam所产生新的像素的邻接的差。Forward Energy的代价计算函数如下:
def compute_cost_forward(image,energy_map,axis=1):
if axis==0:
image = np.transpose(image,(1,0,2))
energy_map = np.transpose(energy_map,(1,0,2))
image = color.rgb2gray(image)
H,W = image.shape
cost = np.zeros((H,W))
cost[0] = energy_map[0]
paths = np.zeros((H,W))
for i in range(1,W-1):
cost[0,i] = cost[0,i] + np.abs(image[0,i-1] - image[0,i+1])
cost[0,0] = energy_map[0,0]
cost[0,W-1] = energy_map[0,W-1]
for i in range(1,H):
for j in range(W):
if j ==0:
cost_list = np.array([cost[i-1,0],cost[i-1,1]+np.abs(image[i-1,0]-image[i,1])])
index = np.argmin(cost_list)
paths[i,j] = index
cost[i,j]=energy_map[i,j]+cost_list[index]
elif j==W-1:
cost_list = np.array([cost[i-1,W-2]+np.abs(image[i-1,W-1]-image[i,W-2]),cost[i-1,W-1]])
index = np.argmin(cost_list)
paths[i,j] = index-1
cost[i,j] = energy_map[i,j] + cost_list[index]
else:
diff_left = np.abs(image[i,j+1]-image[i,j-1])+np.abs(image[i-1,j]\
-image[i,j-1])
diff_mid = np.abs(image[i,j-1]-image[i,j+1])
diff_right = np.abs(image[i,j+1]-image[i,j-1])+np.abs(image[i-1,j]\
-image[i,j+1])
cost_list = np.array([cost[i-1,j-1]+diff_left,cost[i-1,j]+diff_mid,\
cost[i-1,j+1]+diff_right])
index = np.argmin(cost_list)
paths[i,j] = index-1
cost[i,j] = energy_map[i,j] + cost_list[index]
if axis==0:
image = np.transpose(image,(1,0,2))
energy_map = np.transpose(energy_map,(1,0,2))
return paths,cost
这里需要注意,图像在边缘区域由于一些像素的缺失,所以需要单独处理。我们来比较compute_cost_forward
和compute_cost
的效果:
from carving_seam import reduce
out = reduce(img_yolo, 200, axis=0)
plt.subplot(311)
plt.title('Original Image')
plt.imshow(img_yolo)
plt.subplot(312)
plt.title('Normal cost function')
plt.imshow(out)
# This step can take a very long time depending on your implementation.
out = reduce(img_yolo, 200, axis=0, cfunc=compute_cost_forward)
plt.subplot(313)
plt.title('Forward cost function')
plt.imshow(out)
plt.show()
输出为:
明显可以看出compute_cost_forward
的代价计算方式对于图中恐龙头部的处理更好。
三、用Seam Carving移除图像中某物体
利用Seam Carving算法移除图中某一物体的主要思想为:将图像中物体所对应的像素的能量定义为一个较大的负数,以确保在利用Seam Carving算法时,使得物体所对应的像素能够删除;物体删除后,图像尺寸得以缩小,再利用上面提到的方法把图像扩大为原尺寸即可。代码如下:
def remove_object(image,mask):
H,W,C= image.shape
mask = mask.astype(int)
regions = measure.regionprops(mask)
region = regions[0]
if region.bbox[2]-region.bbox[0]<region.bbox[3] - region.bbox[1]:
image=np.transpose(image,(1,0,2))
mask = np.transpose(mask,(1,0))
image_gray = color.rgb2gray(image)
count = 0
image_new = image.copy()
while not np.all(mask == 0):
energy_map = energy_function(image_gray)
energy_map = energy_map - energy_map * mask * (1000)
paths,cost = compute_cost_forward(image_gray,energy_map)
index = np.argmin(cost[-1])
seam = backtrack_seam(paths,index)
image_gray = remove_seam(image_gray,seam)
image_new = remove_seam(image_new,seam)
mask = remove_seam(mask,seam)
count = count+1
out = enlarge(image_new,image_new.shape[1]+count)
if region.bbox[2]-region.bbox[0]<region.bbox[3] - region.bbox[1]:
out = np.transpose(out,(1,0,2))
return out
需要注意的是这里我们还要输入一个想要删除的物体的mask,利用skimage.measure.regionprops
函数和mask得到图中将要删除物体的位置(最上,最下,最左,最右坐标),然后如果最上的位置坐标减去最下的位置坐标大于最右位置坐标减去最左位置坐标,则说明我们删除沿着从左到右的方向删除seam的数目较少,计算较为简便,那么我们则翻转图像:
from carving_seam import remove_object
out = remove_object(image, mask)
print(out.shape)
plt.subplot(2, 2, 1)
plt.title('Original Image')
plt.imshow(image)
plt.subplot(2, 2, 2)
plt.title('Mask of the object to remove')
plt.imshow(mask)
plt.subplot(2, 2, 3)
plt.title('Image with object removed')
plt.imshow(out)
plt.show()
得到:
我们可以看见删除房子物体后效果并不是特别好,这主要是由于我们沿着从左到右的方向删除seam,如果这个seam包含房子所对应的像素,则房子右边天空所对应的像素不能得到很好保留。为了验证这个说法我们沿着从上到下的方向删除seam:
def remove_object1(image,mask):
H,W,C= image.shape
mask = mask.astype(int)
image_gray = color.rgb2gray(image)
count = 0
image_new = image.copy()
while not np.all(mask == 0):
energy_map = energy_function(image_gray)
energy_map = energy_map - energy_map * mask * (1000)
paths,cost = compute_cost_forward(image_gray,energy_map)
index = np.argmin(cost[-1])
seam = backtrack_seam(paths,index)
image_gray = remove_seam(image_gray,seam)
image_new = remove_seam(image_new,seam)
mask = remove_seam(mask,seam)
count = count+1
out = enlarge(image_new,image_new.shape[1]+count)
return out
这里我们只需对remove_object
稍加修改即可:删除翻转图像的操作。查看输出图像:
# Use your function to remove the object
out = remove_object1(image, mask)
print(out.shape)
plt.subplot(2, 2, 1)
plt.title('Original Image')
plt.imshow(image)
plt.subplot(2, 2, 2)
plt.title('Mask of the object to remove')
plt.imshow(mask)
plt.subplot(2, 2, 3)
plt.title('Image with object removed')
plt.imshow(out)
plt.show()
得到:
这次我们看见效果确实比刚刚好,这次我们可以看见右边的天空;但实际上图片中物体稍稍被拉长。