基于Python实现Seam Carving算法

1, 问题描述 结合 “Lecture 6 Resizing“ 的 Seam Carving 算法,设计并实现前景保持的图像缩放,前景由 gt 文件夹中对应的标注给定

本文包含相关资料包-----> 点击直达获取<-------

1. 问题描述

结合 “Lecture 6 Resizing“ 的 Seam Carving 算法,设计并实现前景保持的图像缩放,前景由 gt 文件夹中对应的标注给定。要求使用“Forward Seam Removing”机制, X, Y 方向均要进行压缩。压缩比例视图像内容自行决定(接近1-2*前景区域面积/图像面积 即可)。需要从测试子集中任选两张代表图,将每一步的 seam removing 的删除过程记录, 做成 gif 动画格式,测试子集的其余图像展示压缩后的图像结果。

2. 求解过程与算法

Seam Carving的基本思想是移除图像中不重要的像素,也就是能量更少的像素。能量高一般来说有较强的轮廓。人类的视觉对边缘更敏感,所以试着从平滑的区域移除内容。则可以使用能量函数: $$ E_1(I)=|\frac\part{\part x}I|+|\frac\part{\part y}I| $$ 也就是以梯度的绝对值作为能量函数,对图像进行处理,可以得到一张关于图像的能量图。对于多通道的图片,对不同通道进行梯度计算然后将多通道的能量进行简单加和即可。能量越大的地方表明该处的梯度更大,因此更加重要。考虑优先删除更不重要的地方,也就是能量图中能量更少的地方。图像需要保证方正,因此考虑每次移除每一行中能量最低的像素。而且这些像素必须相邻才不会破坏图像内容。看上去,每次移除的一组像素就是从图像顶上到底下的一条曲线。

这样一来,删除的应该是能量最低的一条曲线。从上到下、从左往右遍历图片,计算到达当前位置的接缝最小的累计能量,遍历完图片后就能依据累计能量最少的路径来决定删除哪条接缝。其中,累计能量可以用动态规划计算,其动态转移方程为: $$ M(i,j)=E(i,j)+\min(M(i-1,j-1),M(i-1,j),M(i-1,j+1)) $$ 上述方法称为Backward Seam Moving。对于某些图像,采用该方法会产生断层。考虑不移除能量最少的接缝,而是移除插入能量最少的接缝,换句话说,就是去掉接缝后,希望接缝两端像素的差值最小,就能改进该不足,这样就能得到改进的Forward Seam Moving机制。上述的动态转移方程改为: $$ M(i,j)=\min\left{\begin{matrix}{} M(i-1,j-1)+|I(i,j+1)-I(i,j-1)|+|I(i-1,j)-I(i,j-1)|\ M(i-1,j)+|I(i,j+1)-I(i,j-1)|\ M(i-1,j+1)+|I(i,j+1)-I(i,j-1)|+|I(i-1,j)-I(i,j+1)| \end{matrix}\right. $$ 对于某些图片,我们不希望图中的某些部分被删改,因此才需要本题中的前景背景图像,确定前景位置保证前景尽可能不被删除。既然希望前景不被删除,那么就意味着前景部分有着更多的能量,因此,我们只需要将前景所在部分的能量加上一个常数即可。这个常数可以由自己设置,其值越大,表明前景越重要,被删除的优先级越低。即新能量函数为: $$ E_2(I)=\left{\begin{matrix}{E_1(I),当前位置为背景\E_1(I)+k,当前位置为前景}\end{matrix}\right. $$ 因此,算法实现的步骤为:

  1. 计算原图的能量图:
  2. 先计算梯度图
  3. 在梯度图的基础上,在前景像素对应的位置加上一个常数
  4. 依据上述动态转移方程,通过动态规划方法找出能量最少的接缝并删除
  5. 重复以上两步,直到图片大小被裁剪到规定的大小

3. 代码实现与说明

我使用3.7版本的 python 对该算法进行了实现。本实验涉及到图片存取与删改,因此用到了图像处理库 PIL ,为了方便处理,将图像转换为矩阵进行处理,还用到了 numpy 库,为了预览中间结果,还使用了 matplotlib 库。缺少上述组件会导致程序报错。

3.1 预处理与参数设置和说明

首先读入图片,考虑到背景图为RGB图像,但色彩只有黑白,我直接将其转换成了黑白图表示:

python for i in range(57, 1000, 100): img = Image.open("data\\imgs\\"+str(i)+".png") ground = Image.open("data\\gt\\"+str(i)+".png").convert('1')

对于每一张图片,我们首先应该要确定其缩放比例。依据题目要求,图片的应该要缩放到原来的$1-(2*前景面积)/总面积$,但据我观察,如果等比例缩放,有时最终图片不能达到较好的效果,这是因为有的图片在竖直方向上前景比例更小,有的在水平方向上前景比例更小。因此我手动设置了适合每张图片的宽度缩放比例:

python pic_rate = [0.5, 0.8, 0.4, 0.5, 0.7, 0.6, 0.7, 0.7, 0.5, 0.2]

假设当前图片对应的 pic_rate 大小为$\alpha$在计算新图像大小时,在原来的缩放比例 rate 基础上,宽度的缩放比例变为$rate^\alpha$,而高度的缩放比例变为$rate^{1-\alpha}$,这样一来可以保证图片压缩率的同时,针对图片内容进行效果更优的缩小。代码实现如下:

python size = img.size front_ground = np.sum(np.array(ground) == 1) # 统计前景面积 rate = 1 - (2*front_ground / (size[0]*size[1])) # 计算缩小比例 new_size = (int(size[0]*(rate**pic_rate[i//100])), int(size[1]*(rate**(1-pic_rate[i//100])))) # 进一步计算宽度和高度分别的缩小比例

3.2 对高度和宽度裁剪的说明

针对宽和高的Seam Carving过程,我设计了以下两个函数:

python def widthCarve(img, ground, new_width, save_frames, mode) def heightCarve(img, ground, new_height, save_frames)

其中, img 为原图, ground 为前景图, new_width new_height 为最终输出图像的宽或高, save_frames 为保存每次中间结果的选项,如果为 True 则会保存每一帧以便之后制作gif图像。 mode 表示保存 gif 的一帧时是否要转置图片。两个函数都返回最终得到的图片和对应的前景图。

需要说明的是,对宽度的裁剪和对高度的裁剪具有高度相似性:二者执行算法的流程是一致的,只是接缝的方向不同。因此,代码很大程度上可以重用。实际上,删除图片的横向接缝,相当于将图片转置后删除图片的竖向接缝,因此,在实现高度裁剪时,只需要转置图片再调用宽度裁剪即可。得到图片后,再转置回来,从而得到最终结果:

```python

高度裁剪

def heightCarve(img, ground, new_height, save_frames): # 转置 img = img.transpose(Image.TRANSPOSE) ground = ground.transpose(Image.TRANSPOSE) # 调用宽度裁剪 img, ground = widthCarve(img, ground, new_height, save_frames, 2) # 转置回来 img = img.transpose(Image.TRANSPOSE) ground = ground.transpose(Image.TRANSPOSE) # 将帧号变为0 global frame frame = 0 return img, ground ```

我的算法是先进行宽度裁剪再进行高度裁剪,因此高度裁剪完成意味着当前图片处理完成,全局变量 frame 表示帧号,应重新置为0。

3.3 算法实现主体

下面的内容在函数 widthCarve 中实现。在实现对图片的裁剪时,要一直修剪到图片和预期大小相同为止,因此所有的算法在一个大循环中:

python while True: width = img.size[0] # 宽度达到要求则退出循环 if width == new_width: break

为了方便计算,先将图片对象转换为 numpy 的数组对象:

python img_array = np.array(img).astype(np.int8) ground_array = np.array(ground).astype(np.int8)

然后计算能量图,即梯度图。对于RGB三通道图片来说,只需要分别计算三个通道的梯度图然后进行相加即可,注意梯度要取绝对值:

python red_gradient = np.array(np.gradient(img_array[:,:,0])) red_gradient = np.maximum(red_gradient, -red_gradient) green_gradient = np.array(np.gradient(img_array[:,:,1])) green_gradient = np.maximum(green_gradient, -green_gradient) blue_gradient = np.array(np.gradient(img_array[:,:,2])) blue_gradient = np.maximum(blue_gradient, -blue_gradient) energy_map = red_gradient[0] + red_gradient[1] + green_gradient[0] + green_gradient[1]+ blue_gradient[0] + blue_gradient[1] energy_map = np.array(energy_map).astype(np.int16)

将前景部分增加能量的部分,我在之后的计算累计能量时进行了实现,而没有直接加在能量图上。

还需要初始化以下变量:

python cumulate_energy = np.zeros(energy_map.shape)# 累计能量图,使用动态规划更新的对象 path = np.zeros(energy_map.shape).astype(np.int16)# 路径图,记录最少能量的路径 new_img = np.zeros((energy_map.shape[0],energy_map.shape[1]-1,3)).astype(np.int8) # 删除接缝后的图片 new_ground = np.zeros((energy_map.shape[0],energy_map.shape[1]-1)).astype(np.int8) # 删除接缝后的前景背景图

cumulate_energy 即为累计能量图, cumulate_energy[i,j] 表示从图片顶部到达 [i,j] 位置的接缝所需的最少能量,而 path 用于记录该能量最少接缝的实际路径。如:到达 [i,j] 处能量最少的接缝的上一处位置为 [i-1,j-1] ,则 path[i,j]=j-1 。因为从上往下的接缝其行坐标依次增加,很容易得知,所以只需要保存列坐标即可。

对于第一行,直接进行赋值:

python cumulate_energy[0,:] = energy_map[0,:] # 累计能量图和能量图的第一行相同 path[0,:] = -1 # 第一行路径为-1表示路径终止

之后的内容,依次遍历图像的每个像素并通过动态规划方法更新即可。遍历到位置 [i,j] 时,计算从左、中、右来的三条接缝分别的累计能量:

python left = cumulate_energy[i-1,j-1] + abs(energy_map[i,j+1] - energy_map[i,j-1]) + abs (energy_map[i-1,j] - energy_map[i,j-1]) mid = cumulate_energy[i-1,j] + abs(energy_map[i,j+1] - energy_map[i,j-1]) right = cumulate_energy[i-1,j+1] + abs(energy_map[i,j+1] - energy_map[i,j-1]) + abs (energy_map[i-1,j] - energy_map[i,j+1])

需要说明的是,如果在边界处,即 j 为0或 img.size[0]-1 时,上述接缝只有两条且下标会出界,因此在边界处按照Backward方法计算能量即可,即:

python if j == 0: # 第一列不能有从左边来的路径 right = cumulate_energy[i-1,j+1] + energy_map[i,j] mid = cumulate_energy[i-1,j] + energy_map[i,j] elif j == img.size[0]-1: # 最后一列不能有从右边来的路径 left = cumulate_energy[i-1,j-1] + energy_map[i,j] mid = cumulate_energy[i-1,j] + energy_map[i,j]

只需要将 left right mid 初始化为一个很大的值,就能在有边界条件时也不会选到非法位置。得到三条不同接缝的累计能量后,选择最小的,并更新对应的累计能量图和路径即可。 choosePath 函数用于找出累计能量最小的路径,当 left mid right 分别为最小值时会分别返回-1、0、1。另外,如果当前位置是前景则额外再加上一个常数,我设置的是2550。

```python

找到累计能量最小的路径,记录累计能量和路径

p = choosePath(left, mid, right) cumulate_energy[i,j] = (left, mid, right)[p+1]

如果当前像素是前景,则加入惩罚,即增加当前能量

if ground_array[i,j] == 1: cumulate_energy[i,j] += 2550 path[i,j] = j+p ```

当整张图遍历后, cumulate_energy 的最后一行最小的值对应的接缝就是整张图中能量最少的接缝。我们只需要从下往上通过之前保存好的 path 找出该路径即可。从下往上遍历的同时,一行行将删除当前行接缝像素后的图片赋值给新图片:

python for i in range(img.size[1]-1, -1, -1): bias = 0 # 遇到删除点前,偏移为0 for j in range(img.size[0]-1): if j == pos: # 遇到删除点,偏移加一 bias = 1 # 将该行除了删除点外的像素赋值给新图片 new_img[i,j,:] = img_array[i,j+bias,:] new_ground[i,j] = ground_array[i,j+bias] # 通过path查询上一行删除的像素点所在列 pos = path[i, pos]

如果需要保存gif,在将上述路径删除前先赋值上色再保存即可,代码基本一致,不再赘述。需要注意的是,在高度裁剪时,使用的是转置的图片,因此保存的图片需要再进行一次转置才能恢复原来的形状。转移与否是通过之前提到的函数参数列表中的 mode 控制的。

4. 结果展示与分析

我的学号尾号是57,因此使用的图片为57、157、257、...、957一共十张图。最终生成的图片在我提交的文件中下面的文件夹里,包含10张图片的结果、编号为57和557这两张图片的gif以及gif的每一帧:

output\SeamCarving

我在提交的文件中,为了便于查看,将 output 文件夹放在了根目录下,但在实际运行代码时,需要将 output 文件夹放在 code 文件夹下,否则程序运行时会报错。

十张图片处理前后的结果分别如下,分别为原图和处理后的图片:

我将图片缩放比例作为中间结果输出,得到了如下结果:

可以看到,第5张图片按照题目要求的公式,需要缩放到原来的1/5,图片缩的过小,因此效果并不太好,但其他图片相对原图都有较优的效果,特别是第7、8张图片,在长宽缩小程度超过原图的情况下仍然有较好的效果。

选取两张图的中间图片生成gif,我选择了编号为57和557的两张图,因为这两张图中,有多个前景主体,且在图片中的位置相对分散,其压缩过程更能体现Seam Carving算法的有效性:

生成的gif和gif的每一帧结果都在上述的文件夹下。

总之,Seam Carving算法是基于内容感知的图像缩放算法,通过上述实验不难发现,在图片缩小的过程中,图片主体部分得到了较好的保留,从而使得图片内容与信息得到了更好的保留。

参考文献

  • 基于贪心算法的物流配送系统设计与实现(西北师范大学·柴荣)
  • 社会化智能音乐发现系统设计与实现(复旦大学·丛洋洋)
  • 基于Storm的选股回测与计算系统的设计与实现(南京大学·史佳栋)
  • Q建材企业砂浆罐车指派算法研究及系统开发(河南理工大学·常梦辉)
  • 基于JBPM的大数据挖掘服务流程引擎的研究与实现(福州大学·高鹏)
  • 一种路径规划任务管理系统的设计与实现(南京大学·沈佳楠)
  • 主题网络爬虫关键技术研究(哈尔滨工业大学·王桂梅)
  • 基于Mahout的分布式视频推荐系统的研究与实现(大连理工大学·高拓)
  • 个性化音乐推荐系统的设计与实现(华中科技大学·余梦琴)
  • 带有装载约束的车辆路径优化问题研究与应用(西安建筑科技大学·李俊)
  • 集装箱接驳运输建模与优化实验平台的设计与实现(东北大学·王新鑫)
  • 面向概念漂移问题的推荐系统研究(电子科技大学·谭轲)
  • 个性化音乐推荐系统的设计与实现(华中科技大学·余梦琴)
  • 个性化音乐推荐系统的设计与实现(华中科技大学·余梦琴)
  • 基于WebGIS的物流运营及信息管理系统研发(西安科技大学·胡航)

本文内容包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主题。发布者:毕设工坊 ,原文地址:https://bishedaima.com/yuanma/36072.html

相关推荐

发表回复

登录后才能评论