原论文:https://people.csail.mit.edu/kaiming/sig13/index.html
摘要
拍摄全景照片的时候,因为人手在抖,所以可能拍出来的图片边界就不是一个矩形框。在本文中,作者提出了一种可以感知图片内容的弯曲(warp)算法,来将这种形状非矩形的图片转化为矩形图片。分为两步,第一步是没有mesh的,初步把一张图片弯曲成矩形。第二步就是在矩形中建立mesh,然后在其中优化图片,使得图片中的各种形状保持原样(例如直线保持直线的样子)。
简介
同上介绍的,关于拍摄全景照片,因为手抖,无法拉出一个矩形图片。
一个简单的解决办法是:剪裁。从这个图片中剪裁出一个矩形区域。缺点是会损失很多信息。还有一种方法就是在原来的图片边缘的外面进行图片合成,缺陷是,对于复杂的语义信息无法处理。
作者这里提出了一种只会引入“可接受”的、“不容易注意到”的扭曲,并且保护图片原本内容的方法。
前人工作
Image alignment and stitching
全景照片实际上是将多张照片缝合(stitching)在一起的。通常用某种图片上的特征,来将原照片全部投影到一个相同的坐标系下。
Projections
众所周知,3D场景投影到2D会不可避免地导致一些折叠的扭曲。例如透视投影,虽然可以保持直线的形状,但是其他物体的形状则会被拉伸。圆柱投影和球面投影可以保持物体的形状,但会弯曲直线。
也有研究者提出了一种在自适应流形(adaptive manifold)上进行投影和缝合的方法。不过它是适应相机的移动,而不是适应图片的内容。
之前也有大量的工作,考虑将这些折叠后的扭曲,通过某种方式,让它们出现在不容易注意到的位置。虽然视觉上可以接受,但是无法移除固有的扭曲。
Image retargeting and warping
Image retargeting是一种基于图片内容的缩放图片的方式。但是它们都假设输入的图片具有矩形形状,而且其中一部分必须还要定义一个grid mesh。
Image completion
即前面提到的,在图片的边缘增加内容,使其变成一个矩形图片。一般的方法是把图片里面的内容,复制到需要合成的地方。但是对于需要生成语义内容来说,表现不好。
算法
算法分为两部分,一部分是改进的Seam Carving,是一个Local Warp的步骤,其将图片初步弯曲成矩形。然后另一部分(global warp)在前一步的基础上,建立一个grid mesh,在mesh上进行优化,来保证原图的物体形状和直线形状不变。
Local Warp
原版的Seam Carving算法是在图片中插入水平或者垂直的接缝(seam),来穿过整张图片,然后再从接缝中水平或者垂直地扩展一个像素。作者这里想到,接缝不穿过整张图片,这样就可以改变图片边缘的形状,从而变成矩形。
原版的算法可见https://faculty.runi.ac.il/arik/scweb/imret/index.html
1.jpg
如上图,其中蓝色的部分是有效的图片像素,白色的部分是需要我们填充的像素。
首先定义boundary segment是待填充像素中的,在图片顶部/底部/左侧/右侧的一行/一列像素。例如上图。我们找到目前最长的一个boundary segment,然后从它扩展得到一个sub-image。
这里左侧右侧的boundary segment,扩展出来的sub-image的高度保持,而宽度则等于整张图。同理,顶部和底部的boundary segment,扩展出来的sub-image的宽度保持,高度等于整张图。
然后我们在这个sub-image上利用seam carving的方法,去进行图像扩展。方法很简单,首先根据图片颜色梯度信息,计算每一个像素的能量。然后使用动态规划,求解最短路径。
以Figure 3(ii)为例,计算得到每个像素的能量。因为我们需要向右边填入像素,所以我们需要一个竖直方向的seam。于是我们用动态规划的方式,求底部像素到顶部像素的最短路(8邻居路径),然后所有底部像素中距离顶部像素最近的像素,及其到顶部的路径,成为我们的seam。
注意这里使用8邻居是源于原算法的研究,另外,我们需要给待填入的像素的能量赋值为108或者更大的数,来防止seam穿过这些像素。
填充像素的时候,我们把seam及其右边的像素都往右移动一位。原本seam的位置则求取左右两个像素的平均。
原版算法中提到,为了防止反复在相同的位置插入seam、导致如下的情况
2.jpg
之前的作者建议同时采集多根seam,然后依次填入像素。
但是这在我们这篇文章中是行不通的。我们的Local Warp要求我们每次填充完一根seam之后,都要重新计算最长的bound segment,我们不能反复对同一个sub-image填入像素。
我这里的实现采用了一个比较trick的方法,算出seam后,对原本的seam的位置的能量设置为105这样的数,比一般的大,比非法像素的小。之后让像素和能量都右移。现在原本的seam位置的能量和其右边一个像素的能量都为105了,这样就有更少的可能会在这里再次插入seam。
总而言之,重复以上过程,直到填充完所有像素。例如我实现的下例
3.jpg
其原图为
4.jpg
可以看到,虽然填充是填充了,但是歪歪扭扭。于是还需要下面的步骤。
Global Warp
Mesh Placement
首先,我们在Local Warp之后的图片的长宽上,将其分成(40−1)∗(10−9)个网格,其中40∗10是网格的顶点数量。网格是正交的。
在Local Warp中,我们对像素进行了移动,我们需要记录下每个像素移动的偏移量。然后,我们使用网格顶点所在像素的偏移量,将各个网格顶点移动回去,就完成了mesh placement这一步。之后local warp的图像就可以抛弃了。移动后的结果例如:
5.jpg
Energy Function
这里设计能量的目的有三个,对于目前的网格与最终的网格结果之间:
- 保持网格的形状尽量不变,亦即Shape Preservation
- 保持网格内的线段的笔直,并且与其平行的其他线段,在处理后依然平行,亦即Line Preservation
- 网格上下左右贴合目标矩形图像框,亦即Boundary Constraints
Shape Preservation
最终输出的mesh的顶点(vertex)集记作V,其中的每一个元素vi=(xi,yi),可以用坐标表示出来。我们进行完Mesh Placement后得到的初始mesh记作V^
保型的能量定义为
ES(V)=N1q∑∣∣(Aq(AqTAq)−1AqT−I)Vq∣∣2
这里N是quad的个数。其中
Aq=⎣⎡x^0y^0⋮x^3y^3−y^0x^0⋮−y^3x^310⋮1001⋮01⎦⎤
Vq=⎣⎡x0y0⋮x3y3⎦⎤
这里也就是说第q个quad的四个顶点的坐标。其中Aq中的是初始mesh的坐标,Vq是目标mesh的坐标。
Line Preservation
我们目前计算过的东西是不包含“线段”这样的信息的。我们首先要做的事是,使用任何一种线段检测算法,来算出图片中的线段。
根据作者的说法,我们需要将这些线段切分开来,放到mesh的各个quad里。然后将所有线段的方向角(范围[−2π,2π)),离散化为M=50个角度,每个离散化角度称为一个bin。为了保持直线性和平行性,设计的能量需要让每个bin里的线段拥有同样的角度。
给定一个线段,其方向向量e可以由线段的两个端点相减得到。如果我们将线段的两个端点用其所在的quad的Vq进行双线性插值表示,那么e就可以表示为Vq的线性函数。
同样的e是我们的目标,而e^则是最初的对应线段的方向向量。对于某一条线段,给定旋转操作的目标角度θm,它的能量为
∣∣sRe^−e∣∣2
其中
R=[cosθmsinθm−sinθmcosθm]
是旋转矩阵,而
s=(e^Te^)−1e^TRTe
是放缩因子。能量可以重新写为
∣∣Ce∣∣2
其中
C=Re^(e^Te^)−1e^TRT−I
因为e^和e都可以写作V^q和Vq的线性函数,所以能量就可以写成二次函数。
总的能量为
EL(V,{θm})=NL1j∑∣∣Cj(θm(j))eq(j)∣∣2
这里NL为线段总数。对于第j条线段,θm(j)代表其所属的bin中的目标旋转角度,q(j)代表其所属的quad。
Boundary Constraints
直白点说,就是把mesh的上下左右拉到图片框边缘上。
EB(V)=vi∈L∑xi2+vi∈R∑(xi−w)2+vi∈T∑yi2+vi∈B∑(yi−h)2
L/R/T/B分别是左右上下边界的mesh顶点,w,h则是图片框的宽高。
最终能量
将前面的几个加和,添加权重
E(V,{θm})=ES(V)+λLEL(V,{θm})+λBEB(V)
显然,我们需要图片至少能够贴合图片框,我们设置λB=108来确保这一点。然后,关于λL的选择,作者说选取>10比较好,原文采取了λL=100。
优化算法
作者指出,可以分别优化V和{θm}。每各优化一次算迭代一次,总共迭代10次。
固定{θm}优化V
此时能量函数是V的二次函数,可以求解线性方程来计算。总共就几百个顶点,所以总体上来说,这部分的开销很小。
这里的求解是通过最小二乘法来算的,具体见我自己的实现部分。
固定V优化{θm}
因为另外两项和θ无关,所以我们只用优化EL这一部分。
作者指出,可以通过牛顿迭代等方法去优化。但是,几何意义上,EL的目标是找到一个θm,使得所有在第m个bin中的线段e和其初始线段e^的夹角,都近似等于θm。所以,我们可以简单的计算所有这个夹角,然后加和,求平均,作为新的θm。
防止拉伸和后处理
如果我们的目标图相框的长宽比设置的不是很好,那么我们的结果图就会被拉伸。为了减少这种拉伸问题,我们在global warping之后更新目标框。
对于每个quad,我们计算它的x方向的伸缩因子为
sx=(xmax−xmin)/(x^max−x^min)
所有quad的平均伸缩因子就为sˉx。同理可以计算出sˉy。之后我们的目标框就设置为(w/sˉx,h/sˉy)。
之后我们再在这个新的目标框上跑一次global warping
获得最终的mesh之后,我们就可以通过一些方法来获得最终的图像了。以图形学的思想为例,最终的mesh构成了我们要绘制的三角面。三角面空间坐标为最终mesh的坐标,uv坐标则是初始mesh的坐标。texture则是原图。
作者指出,这样的实现有一个小问题,因为mesh不会只包住有效像素,例如
6.jpg
这里的quad有一部分就包含了外部的无效像素。进行纹理采样时,作者简单地将无效像素替换为最近的有效像素。
实现细节
由于mesh是很“光滑”的,作者指出我们可以在缩小的图片上进行mesh的计算。首先将图片缩小到一个固定的大小,然后再在上面进行local/global warping。然后我们将mesh的尺度放大到原来的图片大小,再用这个mesh去进行纹理采样。
本文局限
如果图片缺失的部分太多,那么处理结果不好
7.jpg
本文的算法可能会导致边缘凹进去
8.jpg
作者指出,可以手动添加一个透明区域,当作已知像素。
9.jpg
将其当作已知像素来warp,之后在本文的算法处理完之后,再在这个透明区域中使用image completion等方法。
另外,本文无法处理未识别到的线段。这可以通过用户交互来缓解。
最后,本文无法应对线段数量过多的情况,这是warping方式的共同挑战。
我的实现
https://github.com/kegalas/Rectangling_Panoramic_Images
一些细节
首先是我之前也提到过的,为了防止反复在同一个地方插入seam,我们添加了额外的一个像素类别SEAM_PIXEL,让他的能量为105。比一般的像素大,比无效像素小。
在Global Warping主要要说明的部分则是关于如何优化能量。从ES开始
ES(V)=N1q∑∣∣(Aq(AqTAq)−1AqT−I)Vq∣∣2
我们需要使其最小。众所周知,使向量的范数平方最小,只用使其每一维都最小即可。或者说,使向量尽可能成为零向量。
这里的(Aq(AqTAq)−1AqT−I)是一个8×8矩阵(记作A好了),而Vq是一个8维向量。我们可以将所有的Vq收尾接在一起。如果我们的mesh是40×10的,那么新的拼接向量VQ的维度就是(40−1)×(10−1)×8
VQ=⎣⎡V1V2⋮VN−1VN⎦⎤
同理,我们也可以把A拼接。如下
AQ=⎣⎡A1A2⋱AN⎦⎤
我们的目标是使得
ES(V)=N1AQVQ=0
这里AQ是用V^算的,而VQ是我们最终mesh的坐标。也就是说我们需要解方程算出来VQ。这其实就是最小二乘法。我们使用Eigen的QR分解对AQ分解,然后再求解VQ。这里建议使用稀疏矩阵。
另外,每一个mesh的顶点只能有一个实例,不能单纯地把所有quad的四个顶点放进去同一列。每个顶点只能出现在VQ里一次。我们直接存储VQ为40×10×2维向量。即每个顶点都只存一份(包含x,y坐标)。然后我们去修改AQ的顺序就可以在向量乘法的时候一一对应。具体见我写的代码。调整后写为
ES(V)=N1AV=0
接下来比较简单的是EB
EB(V)=vi∈L∑xi2+vi∈R∑(xi−w)2+vi∈T∑yi2+vi∈B∑(yi−h)2
对于mesh上的一个顶点vi,其有四种情况,在左侧、右侧、上部,下部。当然有四个顶点同时属于两种情况。也就是说,有一些顶点需要限制x坐标,有一些需要限制y坐标,有一些两个都需要限制,而有一些则两个都不需要限制。
设Bi如下
Bi=[a00b]
一个点如果需要限制x坐标,则a=1,如果需要限制y坐标,则b=1。否则都等于0。
例如左上角的顶点(x,y),我们的目标是
[1001][xy]=[00]
对于右下角的顶点,目标是
[1001][xy]=[wh]
对于右侧的顶点,目标是
[1000][xy]=[w0]
对于中间的无限制顶点,目标是
[0000][xy]=[00]
以此类推。
同样的,我们把vi合并成一个40×10×2向量(同前)。将Bi也合并。
B=⎣⎡B1B2⋱BN⎦⎤
这里就不用重新调整B内容的顺序了。接下来合并目标vti
VT=⎣⎡vt1vt2⋮vtn⎦⎤
最终的目标是
EB(V)=BV=VT
同样,Eigen解方程可得VQ。
接下来是很复杂的EL
设原始的一条线段为L^=[x^l0,y^l0,x^l1,y^l1]T。根据我们前面的说法,一条线段会被mesh切割,然后放进各个quad中。我们用quad的四个顶点来进行双线性插值,来表示目标的L。方式如下,首先用双线性插值从V^q计算出Vq
⎣⎡x^0⋮x^3y^0⋮y^3x^0y^0⋮x^3y^31⋮1x^0⋮x^3y^0⋮y^3x^0y^0⋮x^3y^31⋮1⎦⎤FL=⎣⎡x0y0⋮x3y3⎦⎤
我们把上式左边的8×8矩阵记作W,上式就为WFL=Vq
然后用L^表示L,有
⎣⎡x^l0x^l1y^l0y^l1x^l0y^l0x^l1y^l111x^l0x^l1y^l0y^l1x^l0y^l0x^l1y^l111⎦⎤FL=⎣⎡xl0yl0xl1yl1⎦⎤
上式左侧4×8矩阵记作H,上式就为HFL=L
于是
L=HFL=HW−1Vq
H和W都为已知,所以L表示为Vq的线性函数。计算L的向量e使用两个端点相减,矩阵为
D=[−100−11001]
有
e=DL=[−100−11001]⎣⎡xl0yl0xl1yl1⎦⎤=[xl1−xl0yl1−yl0]
EL中的∣∣Ce∣∣2中的e最终就表示为
(2,1)e=(2,4)D(4,8)H(8,8)W−1(8,1)Vq
而
C=Re^(e^Te^)−1e^TRT−I
其中e^已知,R只和θm有关。可以直接算,与Vq无关。
同样的,我们把所有线段的e和其C拼接(具体见代码),有
EL(V,{θm})=NL1Ce=0
同样的,在优化Vq时,上式是Vq的线性函数,只需要用Eigen解方程即可。
最终,我们的目标是使得
E=ES+λLEL+λBEB
最小,即
⎣⎡N1ANLλLCDHW−1λBB⎦⎤V=⎣⎡00VT⎦⎤
用Eigen求解出V即可。注意其中的CDHW−1指的是每一个线段的矩阵拼接后的结果,具体拼接见代码。