动态规划的实际应用:图片压缩算法

本文介绍了动态规划在实际问题中的应用,特别是图片处理中的接缝裁剪算法。接缝裁剪允许在保持图片内容自然的情况下进行压缩。算法通过定义图片的能量,使用动态规划找到最低能量接缝,从而实现图片的压缩。文章详细阐述了算法的实现步骤,包括能量函数、动态规划搜索、后向指针等关键概念,并提供了实际案例分析。
摘要由CSDN通过智能技术生成

今天推送的第一篇文章《动态规划只能用来求最值吗?》给大家讲解了如何在动态规划算法中求最优解的具体方案。

很多时候大家觉得动态规划算法没什么实际作用。一方面是因为 LeetCode 上很多题目是简化版,只是让你求一个「最大值」,而不是真正去求最优解。另一方面可能是因为真的没有接触过实际场景中的动态规划算法。

那么,今天就给大家看一篇文章,讲述动态规划算法在图片处理中的一个实际应用 —— 接缝裁剪(seam carving)。这篇文章的作者是 Avik Das,原文是英文文章,我将其翻译为了中文。

什么是所谓「接缝裁剪」呢?一句话概括,就是对图片进行横向或纵向的压缩,且压缩后的结果很自然。看一段视频来直观感受一下吧:

这篇文章我认为写的非常好,使用实际的案例帮大家理解动态规划算法,有详细的例子和图解。同时,这篇文章对动态规划的求解过程也讲解得非常清晰,完全按照我所讲的「动态规划解题四步骤」来求解。文章中也详细讲解了「back 数组」的技巧,可以仔细体会。

以下是文章内容。


  • 原文作者:Avik Das[1]

  • 原文地址:Real-world dynamic programming: seam carving[2]

  • 译者:nettee

我们一直认为动态规划(dynamic programming)是一个在学校里学习的技术,并且只是用来通过软件公司的面试。实际上,这是因为大多数的开发者不会经常处理需要用到动态规划的问题。本质上,动态规划可以高效求解那些可以分解为高度重复子问题的问题,因此在很多场景下是很有用的。

在这篇文章中,我将会仔细分析动态规划的一个有趣的实际应用:接缝裁剪(seam carving)。Avidan 和 Shamir 的这篇文章 Seam Carving for Content-Aware Image Resizing[3] 中详细讨论了这个问题以及提出的技术(搜索文章的标题可以免费获取)。

环境敏感的图片大小调整

为了用动态规划解决实际问题,我们需要将问题建模为可以应用动态规划的形式。本节介绍了这个问题的必要的准备工作。

论文的原作者介绍了一种在智能考虑图片内容的情况下改变图片的宽度或高度的方法,叫做环境敏感的图片大小调整(content-aware image resizing)。后面会介绍论文的细节,但这里先做一个概述。假设你想调整下面这个冲浪者图片的大小。

一个冲浪者在平静的海面中间清晰可见的俯视图,右边是身后汹涌的海浪。图片来自 Pixabay 上的 Kiril Dobrev [4]

论文中详细讨论了,有多种方法可以减少图片的宽度。我们最先想到的是裁剪和缩放,以及它们相关的缺点。删除图片中部的几列像素也是一种方法,但你也可以想象得到,这样会在图片中留下一条可见的分割线,左右的内容无法对齐。而且即使是这些方法全用上了,也只能删掉这么点图片:

尝试通过裁掉图片的左侧和中间部分来减少图片宽度。裁掉中间会在图片中留下一条可见的分割线。

Avidan 和 Shamir 在他们的论文中展示的是一个叫做接缝裁剪的技术。它首先会识别出图片中不太有意义的“低能量”区域,然后找到穿过图片的能量最低的“接缝”。对于减少图片宽度的情况,接缝裁剪会找到一个竖向的、从图片顶部延伸到底部、下一行最多向左或向右移动一个像素的接缝。

在冲浪者的图片中,能量最低的接缝穿过图片中部水面最平静的位置。这和我们的直觉相符。

冲浪者图片中发现的最低能量接缝。接缝通过一条五个像素宽的红线来可视化,实际上接缝只有一个像素宽。

通过识别出能量最低的接缝并删除它,我们可以把图片的宽度减少一个像素。不断重复这个过程可以充分减少图片的宽度。

宽度减少了 1024 像素后的冲浪者图片。

这个算法删除了图片中间的静止水面,以及图片左侧的水面,这仍然符合我们的直觉。和直接剪裁图片不同的是,左侧水面的质地得以保留,也没有突兀的过渡。图片的中间确实有一些不是很完美的过渡,但大部分的结果看起来很自然。

定义图片的能量

这个算法的关键在于找到能量最低的接缝。要做到这一点,我们首先定义图片中每个像素的能量,然后应用动态规划算法来寻找穿过图片的能量最低的路径。下一节中会详细讨论这个算法。让我们先看看如何为图片中的像素定义能量。

论文中讨论了一些不同的能量函数,以及它们在调整图片大小时的效果。简单起见,我们使用一个简单的能量函数,表达图片中的颜色在每个像素周围的变化强烈程度。为了完整起见,我会将能量函数介绍得详细一点,以备你想自己实现它,但这部分的计算仅仅是为后续动态规划作准备。

左半边表示,当相邻像素的颜色非常不同时这个像素的能量大。右半边表示,当相邻像素的颜色比较相似时像素的能量小。

为了计算单个像素的能量,我们检查这个像素左右的像素。我们计算逐个分量之间的平方距离,也就是分别计算红色、绿色、蓝色分量之间的平方距离,然后相加。我们对中心像素上下的像素进行同样的计算。最终,我们将水平和垂直距离相加。

唯一的特殊情况是当像素位于边缘,例如左侧边缘时,它的左边没有像素。对于这种情况,我们只需比较将其和右边的像素比较。对于上边缘、右边缘、下边缘的像素,会进行类似的调整。

当周围像素的颜色非常不同时,能量函数较大;而当颜色相似时,能量函数较小。

冲浪者图片中每个像素的能量,用白色显示高能量像素、黑色显示低能量像素来可视化。不出所料,中间的冲浪者和右侧的湍流的能量最高。

这个能量函数在冲浪者图片上效果很好。然而,能量函数的值域很广,当对能量进行可视化时,图片中的大部分像素看起来能量为零。实际上,这些区域的能量只是相对于能量最高的区域比较低,但并不是零。为了让能量函数更容易可视化,我放大了冲浪者,并调亮了该区域。

使用动态规划搜索低能量接缝

为每个像素计算出了能量之后,我们现在可以搜索从图片顶部延伸到底部的低能量接缝了。同样的分析方法也适用于从左侧延伸至右侧的水平接缝,可以让我们减少原始图片的高度。不过,我们现在只关注垂直的接缝。

我们先定义最低能量接缝的概念:

  • 接缝是像素的序列,其中每行有且仅有一个像素。要求对于连续的两行, 坐标的变化最多为 1,这保证了这是一条相连的接缝。

  • 最低能量接缝是指接缝中所有像素的能量总和最小的一条接缝。

注意,最低能量接缝不一定会经过图片中的最低能量像素。是让接缝的能量总和最小,而不是让单个像素的能量最小。

贪心的方法行不通。过早选择了低能量像素后,我们陷入了图片的高能量区域,如图中红色路径所示。

从上图中可以看到,“从最顶行开始,依次选择下一行中的最低能量像素”的贪心方法是行不通的。在选择了能量为 2 的像素之后,我们被迫走入了图片中的一个高能量区域。而如果我们在中间一行选择一个能量相对高一点的像素,我们还有可能进入左下的低能量区域。

将问题分解为子问题

上述的贪心方法的问题在于,当决定如何延伸接缝时,我们没有考虑到未来的接缝剩余部分。我们无法预知未来,但我们可以记录下目前所有已知的信息,从而可以观察过去。

让我们反过来进行选择。我们不再从多个像素中选择一个来延伸单个接缝,而是从多个接缝中选择一个来连接单个像素。 我们要做的是,对于每个像素,在上一行可以连接的像素中进行选择。如果上一行中的每个像素都编码了到那个像素为止的路径,我们本质上就观察了那个像素之前的所有历史。

对每个像素,我们查看上一行中的三个像素。本质的问题是,我们应当延伸哪个接缝?

这表明了可以对图片中的每个像素划分子问题。因为子问题需要记录到那个像素的最优路径,比较好的方法是将每个像素的子问题定义为以那个像素结尾的最低能量接缝的能量。

和贪心的方法不同,上述方法本质上尝试了图片中的所有路径。只不过,当尝试所有可能的路径时,在一遍又一遍地解决相同的子问题,让动态规划成为这个方法的一个完美的选择。

定义递归关系

与往常一样,我们现在需要将上述的思路形式化为一个递归关系。子问题是关于原图片中的每一个像素的,因此递归关系的输入可以简单的是那个像素的 坐标。这可以使输入是简单的整数、使子问题的排序变得容易,也使我们可以用一个二维数组存储计算过的值。

我们定义函数 表示从图片顶部开始、到像素 结束的最低能量的垂直接缝。使用字母

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值