我刚上大学那会儿,课上到最后几分钟的时候,我会翘课奔到另外一个我几乎不怎么了解的班上去蹭课。碰巧,那个班上的课是我觉得最棒的课之一 ——计算机视觉。此外,那个课上介绍了一种很赞的算法:Seam Carving,精雕细琢。 这个算法大概是酱紫的:一般我们想改变图片大小的时候,会采用裁剪和缩放的方式,这样一来,图片会损失很多重要信息,在处理过程中,图片甚至被歪曲。那么,我们怎么才能找到图片中视觉信息最少的部分,要调整图片大小的时候,只把这部分移除掉是不是可以呢? 上图展示给我们一副很美的画面:开阔的蓝天,俊逸的城堡。但是,对我们来说,图有点大,我们得往小调一下。怎么弄呢? |
coraaller
|
第一个进入我们大脑的想法是改变原始图像的尺寸。改变之后的图像(如上图)变小了,而且所有的主要信息(左边的人,右边的城堡)都还在新的图像上。但是,改变后的图像有一个问题,右边城堡变形了,所以这张改变之后的图像就显得不太完美了。在大部分情况下,这种图像的改变是可以接受的,但是如果我们试图提供一个高质量的图像的话,这种改变就不能接受了。 另外一个想法是切掉原始图像的一部分以适应我们新的尺寸(如上图)。基本上我们可以理解发现新的图像有一个致命的缺点,一半的城堡被切掉了,而且左边的人现在也太靠近图像的边缘。相对于原始图像,新的图像确实包含了大部分原始图像的信息,但是同时也丢失了很多的重要信息。我个人就比较喜欢城堡右边的那个炮楼,希望在新的图像中可以保留这个炮楼。幸运的是,我们可以做到这点。 让我们看上面的图像,图像的尺寸已经减小了,在新的图像中,城堡是完整的,并且左边的人也不再位于图像的边缘。上面的这张新的图像是经过一个叫做Seam Carving的算法进行处理过的。这个算法将动态监测原始图像,发现原始图像中不太重要的部分,并且将这部分不太重要的图像有限切除掉。在上面的新的图像中,你可以发现这个算法把城堡右边的蓝天给切除了,而且还切除了部分位于原始图像中间部分的蓝天。 |
HAILINCAI
|
其它翻译版本(1) |
它是如何确定哪些区域应该首先去掉呢?我们通过研究一个Go语言实现的算法来找到答案。我们研究算法的各个步骤,以及每一步对下面的图片产生的效果。这个算法虽然是用来减少图像高度的,但是也可以很容易地修改用来减小图像的宽度。
该算法包含了三个主要的步骤: 从原图生成能量图、 定位找出最低能量消耗的 “seam" , 将找出的”seam“从图像中去除. // ReduceHeight 使用seam carving算法,减少给定的具有n个像素点的图像的高度. func ReduceHeight(im image.Image, n int) image.Image { energy := GenerateEnergyMap(im) seam := GenerateSeam(energy) return RemoveSeam(im, seam) } 能量图计算图像中的一个点包含了多少“能量”,也就是说该点包含了多少信息。低能量的像素同周围像素融合在一起,去掉它们对整个图的影响比较小。因此能量图的计算,采用了考虑图像水平和垂直的梯度值的方法来进行。通过这种方法产生的能量图,其中每个点代表了原始图像中的对应点与周边点相似或不同的程度。 // GenerateEnergyMap 应用输入灰度和sobel滤波器到输入图像上,生成能量图. func GenerateEnergyMap(im image.Image) image.Image { g := gift.New(gift.Grayscale(), gift.Sobel()) res := image.NewRGBA(im.Bounds()) g.Draw(res, im) return res }
|
hxapp2
|
正如所期望的,高能量的区域一般都是边缘,低能量的区域均是由少量相似颜色(天空)扩展而来的。从这里我们可以估计到,减少图片的高度,减少的部分应该大部分都是在天空区域,其他部分保持不变。 下一步决定哪些像素需要进行移除。我们将一个像素一个像素的减少图像的高度,就需要一列一列的找那个像素能够移除。我们希望找到一系列的具有尽可能最低总能量的像素集合,移除掉这些seam,对整个图片产生影响最小。可以按如下两步来确定最佳去除像素点: // GenerateSeam 返回最优的可以消除的水平 seam. func GenerateSeam(im image.Image) Seam { mat := GenerateCostMatrix(im) return FindLowestCostSeam(mat) } 第一步是用一个八连通区域像素去水平滤波整个图像,获得包含“seams"的最低积累能量的消耗矩阵。 这次我们首先看如下代码: // GenerateCostMatrix 从图像左端到每一个像素,创建一个表明最低消耗seam矩阵. // // mat[x][y] 是从图像左端到列x行y像素点的seam的累积能量. func GenerateCostMatrix(im image.Image) [][]float64 { min, max := im.Bounds().Min, im.Bounds().Max height, width := max.Y-min.Y, max.X-min.X mat := make([][]float64, width) for x := min.X; x < max.X; x++ { mat[x-min.X] = make([]float64, height) } // Initialize first column of matrix for y := min.Y; y < max.Y; y++ { e, _, _, a := im.At(0, y).RGBA() mat[0][y-min.Y] = float64(e) / float64(a) } updatePoint := func(x, y int) { e, _, _, a := im.At(x, y).RGBA() up, down := math.MaxFloat64, math.MaxFloat64 left := mat[x-1][y] if y != min.Y { up = mat[x-1][y-1] } if y < max.Y-1 { down = mat[x-1][y+1] } val := math.Min(float64(left), math.Min(float64(up), float64(down))) mat[x][y] = val + (float64(e) / float64(a)) } // Calculate the remaining columns iteratively for x := min.X + 1; x < max.X; x++ { for y := min.Y; y < max.Y; y++ { updatePoint(x, y) } } return mat } |
hxapp2
|
在上面的函数中,我们开始创建一个同图像具有相同维数的矩阵。我们从最左列到最右列,不断的计算每一个像素的最低累积能量。在一列中的每一个像素,选取其左边或者左上或者左下三个点中最小积累能量的点,然后将该点的能量累加到选取的点的积累能量上。这种做法,使得我们能够不是那么死板的只能线性移除seam,带来更大的灵活性,获得更好的清除效果。 然后我们就可以利用这个矩阵来确定哪些像素可以被移除。我们从一个包含每列一个点的seam开始,找到最小成本的seam的开始。 type Seam []Point type Point struct { X, Y int } // FindLowestCostSeam uses an cost matrix to find the optimal seam for removal. func FindLowestCostSeam(mat [][]float64) Seam { width, height := len(mat), len(mat[0]) seam := make([]Point, width) min, y := math.MaxFloat64, 0 for ind, val := range mat[width-1] { if val < min { min = val y = ind } } seam[width-1] = Point{X: width - 1, Y: y} 然后我们从右到左遍历矩阵。每一次循环遍历,查看该点、以及其上和其下三点,将最小的积累能力赋值给该seam。 for x := width - 2; x >= 0; x-- { left := mat[x][y] up, down := math.MaxFloat64, math.MaxFloat64 if y > 0 { up = mat[x][y-1] } if y < height-1 { down = mat[x][y+1] } if up <= left && up <= down { seam[x] = Point{X: x, Y: y - 1} y = y - 1 } else if left <= up && left <= down { seam[x] = Point{X: x, Y: y} y = y } else { seam[x] = Point{X: x, Y: y + 1} y = y + 1 } } 我们通过在图像上画出seam来可视化的检查我们的程序逻辑,确认了seam是通过了我们所期待的区域。下面的图像是上面代码生成的第一个seam,用红色线画在输入图像上。 |
hxapp2
|
因此算法就是通过编写一个函数,该函数创建一个新的去掉了计算出来的seam的图像,并且将ReduceHeight函数放到一个循环中去,我们就可以不断的通过消除最小能量的seam来放大缩小一个图像。 // RemoveSeam creates a copy of the provided image, with the pixels at // the points in the provided seam removed. func RemoveSeam(im image.Image, seam Seam) image.Image { b := im.Bounds() out := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()-1)) min, max := b.Min, b.Max for _, point := range seam { x := point.X for y := min.Y; y < max.Y; y++ { if y == point.Y { continue } if y > point.Y { out.Set(x, y-1, im.At(x, y)) } else { out.Set(x, y, im.At(x, y)) } } } return out } // ReduceHeight uses seam carving to reduce height of given image n pixels. func ReduceHeight(im image.Image, n int) image.Image { for x := 0; x < n; x++ { energy := GenerateEnergyMap(im) seam := GenerateSeam(energy) im = RemoveSeam(im, seam) } return im } 在这里是清除了50个像素后的效果。我们可以看到,哪些具有最少信息的区域(天空)已经清除掉,而哪些有船,水和建筑的区域没有改变。因为天空基本上是一致的,清除这些区域没有太大的影响。 最终的实现代码可以在 Github 上找到,所有函数均能被导出,可以按你想要的方式去研究修改。 |
hxapp2
|
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们
有疑问加站长微信联系(非本文作者)