图像缩放算法主要分为两类,分别是几何变换缩放算法和保持图像内容缩放算法。几何变换缩放算法常用的有均值法、最近领域法以及插值算法等。而基于图像内容的Seam Carving算法,能够较好的保持物体的内容和特征,缩放效果更好。
Python、PyQt5
图像缩放以及物体移除功能
Seam Carving算法的主要思想是:根据图像中的内容来区别对待图像中的像素,首先计算图像中各像素点的能量,来确定每一个像素的重要程度,然后使用动态规划的方法寻找累积能量最小的seam,通过不断地删除或者复制这些累积能量最小的seam,从而实现图像在水平和垂直两个方向上的自适应缩放功能。
为了保持原始图像的内容,被删除或插入的seam必须是重要程度最低的。若像素点的能量值较大,也就是说明其在图像中很重要,那么在缩放图像或者移除物体的时候就要保留该像素点的一些信息。
为了定义seam的重要程度,首先要定义像素点的重要程度。该算法中提出使用如下函数来衡量像素点的重要程度,如果假设图像为I,那么能量函数的定义如下:
实际上,上面的能量计算函数其实就是图像的梯度,梯度能够检测出图像的边缘区域以及主体。容易知道,如果一个像素具有较大的梯度,则说明这个像素很大可能为边缘区域,而边缘区域往往是一幅图像的重要内容。
seam的定义如下:假设的的尺寸为n*m,其中n是行数,m是列数。设垂直的seam为C^V,则C^V可以表示为
其中,x(i)是映射:x(i):[1,...m]→[1,...n]。水平seam的定义与垂直seam类似,便不再赘述。
当缩小图像时,每次删去能量最小的seam来保持图像的重要内容,而当放大图像时,则将能量最小的seam插入到原始图像中,从而保证图像内容不受影响。
1.计算能量图
为了能够得到图像的能量图,需要计算出像素点在x轴的偏导数和y轴的偏导数,然后再将它们的绝对值求和。这里我们采用Sobel滤波器计算图像的偏导数。这是一个在图像上每个通道上计算的卷积核。filter_du是将每个像素替换为它上边的值和下边的值之差,计算的是y轴方向的梯度。filter_dv则是将每个像素替换为它右边的值和左边的值之差,计算x轴方向的梯度。
这种滤波器捕捉到的是每个像素相邻的3×3邻域中像素的总体趋势,对3×3邻域上分别做加权平均和差分运算。其优点在于可以平滑噪声,能够准确的提取图像中的边缘区域信息。
其核心代码如下:
1. def calc_energy_map(self):
2. filter_du = np.array([
3. [1.0, 2.0, 1.0],
4. [0.0, 0.0, 0.0],
5. [-1.0, -2.0, -1.0],
6. ])
7. # 这使它从2D滤波器转换成3D滤波器
8. # 为R, G, B三个通道复制相同的滤波器
9. filter_u = np.stack([filter_du] * 3, axis=2)
10. filter_v = np.array([
11. [1.0, 0.0, -1.0],
12. [2.0, 0.0, -2.0],
13. [1.0, 0.0, -1.0],
14. ])
15. filter_dv = np.stack([filter_dv] * 3, axis=2)
16. img = self.out_image.astype('float32')
17. convolved = np.absolute(convolve(img, filter_u)) + \
18. np.absolute(convolve(img, filter_v))
19. # 我们计算红,绿,蓝通道中的能量值之和
20. energy_map = convolved.sum(axis=2)
21. return energy_map
2.使用后向能量构建累积能量矩阵
对于从上到下的seam,如果一个像素的纵坐标为y,那么上一行组成seam的像素的纵坐标只能是y-1,y,y+1。那么我们为了让能量最小,我们可以选择上一行的这三个像素中能量最小者和该像素组成一条seam,那么通过该点时的总能量为这一点的能量加上累积到上一行的最小能量值。所以可以利用以下的递推公式(从上到下)来计算代价:
其中, M(i,j)表示(i,j)所在位置的累积最小能量,e(i,j)表示(i,j)所在位置的能量。
计算到最后一行时,即为经过最后一行像素的seam的最小总能量。
如下图所示。图片中位于左上角的数字表示该点自身的能量值,右下角的加粗数字表示累积到该点的最小能量值,白色箭头代表可选择的方向,黑色箭头代表得到最小能量值所选择的方向。以第二列为例,第一行本身的能量值以及累积最小能量值均为1,第二行它可选择的左上方,正上方,右上方像素的能量分别为2,1,5;选择最小值即为正上方能量值,再加上本身的能量值9,第二行累积最小能量值则为10;同理,可求得第三行累积最小能量值为6。易知,图4.2中加粗斜体1→4→6,即为一条最小像素线。
核心代码如下:
1.def cumulative_map_backward(self, energy_map):
2. m, n = energy_map.shape
3. output = np.copy(energy_map)
4. for row in range(1, m):
5. for col in range(n):
6. # 选择三个方向上能量最小的一个能量值
7. output[row, col] = energy_map[row, col] + \
8. amin(output[row - 1, max(col - 1, 0): min(col + 2, n - 1)])
9. return output
3.查找并插入从上到下的最小像素线
由上一步已经得到了后向累积能量矩阵,在最后一行中,最小累积能量对应的像素点就是能量最小seam的终点,我们只需要从这一点进行回溯,找出每一行中累积能量最小的索引,并将它们保存在一个数组中即可,这样就得到了能量最小的seam。
首先通过find_seam函数,找到n条最小像素线(n为达到指定宽度需要插入的像素线)。
在插入像素线时,我们对最优seam以及其相邻像素计算平均值,然后将这个平均值复制到最优seam旁来增加一行,以此来放大图像。同时为了防止每次都复制同一个seam(每次都是那个seam最优),可以先将原图中最优的n个seam找出来,然后一次性复制这n个seam,从而增加n行(列)。
若插入位置col是0,则计算出col和col+1列的平均像素值,在col+1的位置,插入该平均值,然后后面的像素值均往后移动一个单位;若插入位置col不为0,则计算出col-1和col的平均像素值,在col的位置插入该平均值,然后后面的像素值均往后移动一个单位。
核心代码如下:
1.def add_seam(self, seam_idx):
2. m, n = self.out_image.shape[: 2]
3. output = np.zeros((m, n + 1, 3))
4. for row in range(m):
5. #col记录最小像素的索引
6. col = seam_idx[row]
7. for ch in range(3):
8. if col == 0:
9. #待插入位置为第一列,p为待插入位置和右边像素的平均值
10. p = np.average(self.out_image[row, col: col + 2, ch])
11. output[row, col, ch] = self.out_image[row, col, ch]
12. output[row, col + 1, ch] = p
13. output[row, col + 1:, ch] = self.out_image[row, col:, ch]
14. else:
15. p = np.average(self.out_image[row, col - 1: col + 1, ch])
16. output[row, : col, ch] = self.out_image[row, : col, ch]
17. output[row, col, ch] = p
18. output[row, col + 1:, ch] = self.out_image[row, col:, ch]
19. self.out_image = np.copy(output)
4.重复步骤 (3),直到达到指定宽度
5.旋转图像增加行并重复步骤 (3) 以调整垂直大小
插入行的操作与插入列的操作类似,只需将图像旋转90°,插入列就转换成插入行的操作。
图像旋转的核心代码如下:
1.def rotate_image(self, image, ccw):
2. m, n, ch = image.shape
3. output = np.zeros((n, m, ch))
4. if ccw:
5. image_flip = np.fliplr(image)
6. #三个通道翻转
7. for c in range(ch):
8. for row in range(m):
9. output[:, row, c] = image_flip[row, :, c]
10. else:
11. for c in range(ch):
12. for row in range(m):
13. output[:, m - 1 - row, c] = image[row, :, c]
14. return output
利用Seam Carving算法移除图像中的关键物体的主要思想是:将图像中物体所对应的像素能量定义为一个较大的负数,以确保在使用Seam Carving算法时,保证物体所对应的像素点能够被删除,物体被删除之后,图像的尺寸会因此而缩小,再利用前文所提到的图像放大的方法,将图像扩大到原尺寸即可。
主要分为以下几步:
1.得到物体宽度和高度,并旋转图像和mask图像
首先在掩膜mask上,通过get_object_dimension()函数获得物体的高度和宽度。因为上传的mask图像是二值图像,被白色(像素值为255)标注的区域即为要去除的物体,其他区域则被标注为黑色(像素值为0),可通过判断像素值计算出物体的高度和宽度。若物体的高度小于宽度,则将图像和mask旋转90°。
其中,get_object_dimension函数的核心代码为:
1.def get_object_dimension(mask):
2.# 记录下所有像素值大于0的坐标值,即物体所在的位置
3. rows, cols = np.where(mask > 0)
4.# 获得物体的高度和宽度
5. height = np.amax(rows) - np.amin(rows) + 1
6. width = np.amax(cols) - np.amin(cols) + 1
7. return height, width
2.计算能量图和修改物体所在像素的能量
使用calc_energy_map函数计算图像中像素的能量值,根据这个能量图,将物体所在的位置的能量值设置成一个较大的负数,这样就能确保物体所在的seam首先被删除。由于是在图像中删除seam,使用前向累积能量计算累积能量矩阵。
3.找出seam并在图像中删除seam
通过find_seam函数,找出最小seam所在位置,使用delete_seam函数删除图像上的seam,同时,也在mask上删除相应的seam。
4.插入像素恢复图像原始大小
将物体移除之后,根据删除的seam数量,使用图像放大的方法,将图像的尺寸恢复到原始大小。若之前旋转了图像,则将图像旋转回初始状态。
核心代码如下:
1.def object_removal(self):
2. """
3. :return:
4. 被mask覆盖的物体所在seam将会首先被移除,然后会插入seam使图像恢复到原始大小
5. """
6. rotate = False
7.# 获得物体的高度和宽度
8. object_height, object_width = self.get_object_dimension()
9. if object_height < object_width:
10. self.out_image = self.rotate_image(self.out_image, 1)
11. self.mask = self.rotate_mask(self.mask, 1)
12. rotate = True
13.# 遍历整个mask图像
14. while len(np.where(self.mask[:, :] > 0)[0]) > 0:
15. energy_map = self.calc_energy_map()
16. # 将物体对应的像素能量定义为一个较大的负数
17. energy_map[np.where(self.mask[:, :] > 0)]*= -self.constant
18. cumulative_map = self.cumulative_map_forward(energy_map)
19. seam_idx = self.find_seam(cumulative_map)
20. self.delete_seam(seam_idx)
21. self.delete_seam_on_mask(seam_idx)
22. print('>', end='')
23.# 计算恢复到原始尺寸需要插入的seam数量
24. if not rotate:
25. num_pixels = self.in_width - self.out_image.shape[1]
26. else:
27. num_pixels = self.in_height - self.out_image.shape[1]
28. self.seams_insertion(num_pixels)
29.# 若移除物体之前,旋转了图像则旋转回来
30. if rotate:
31. self.out_image = self.rotate_image(self.out_image, 0)
图像放大效果,左边显示的是原图以及分辨率,右边为结果图。
系统展示的结果图是按照展示框的大小进行自适应的。因此,另外打开了原图以及缩小后的图片进行了对比,以便效果更加的明显。
用户选择想要去除的带有标注物体的mask掩膜。
第一次写blog,发现我真的不会写,我先多看看别人怎么写的(o(╥﹏╥)o)