最临近插值,通常被用于
图像缩放中,进行缩放图像的方法叫做最临近插值算法,这是一种最基本、最简单的图像缩放算法。效果并不好,放大后的图像有很严重的马赛克,缩小后的图像有很严重的失真;效果不好的根源就是其简单的最临近插值方法引入了严重的
图像失真。
使用情况
一般最临近插值用于图像缩放中
图像的缩放很好理解,就是图像的放大和缩小。传统的绘画工具中,有一种叫做“放大尺”的绘画工具,画家常用它来放大图画。当然,在计算机上,我们不再需要用 放大尺去放大或缩小图像了,把这个工作交给程序来完成就可以了。下面就来讲讲计算机怎么来放大缩小图象;在本文中,我们所说的图像都是指点阵图,也就是用 一个像素矩阵来描述图像的方法,对于另一种图像:用函数来描述图像的矢量图,不在本文讨论之列。
越是简单的模型越适合用来举例子,我们就举个简单的图像:3X3 的256级灰度图,也就是高为3个像素,宽也是3个像素的图像,每个像素的取值可以是 0-255,代表该像素的亮度,255代表最亮,也就是白色,0代表最暗,即黑色 。假如图像的像素矩阵如下图所示(这个原始图把它叫做源图,Source):
234 38 22
67 44 12
89 65 63
这个矩阵中,元素坐标(x,y)是这样确定的,x从左到右,从0开始,y从上到下,也是从零开始,这是图象处理中最常用的坐标系,就是这样一个坐标:
---------------------->X
|
|
|
|
|
∨Y
如果想把这副图放大为 4X4大小的图像,那么该怎么做呢?那么第一步肯定想到的是先把4X4的矩阵先画出来再说,好了矩阵画出来了,如下所示,当然,矩阵的每个像素都是未知数,等待着我们去填充(这个将要被填充的图的叫做目标图,Destination):
? ? ? ?
? ? ? ?
? ? ? ?
? ? ? ?
然后要往这个空的矩阵里面填值了,要填的值从哪里来来呢?是从源图中来,好,先填写目标图最左上角的象素,坐标为(0,0),那么该坐标对应源图中的坐标可以由如下公式得出:
srcX=dstX* (srcWidth/dstWidth) , srcY = dstY * (srcHeight/dstHeight)
好了,套用公式,就可以找到对应的原图的坐标了(0*(3/4),0*(3/4))=>(0*0.75,0*0.75)=>(0,0),
找到了源图的对应坐标,就可以把源图中坐标为(0,0)处的234像素值填进去目标图的(0,0)这个位置了。
接下来,如法炮制,寻找目标图中坐标为(1,0)的象素对应源图中的坐标,套用公式:
(1*0.75,0*0.75)=>(0.75,0)
结 果发现,得到的坐标里面竟然有小数,这可怎么办?计算机里的图像可是数字图像,像素就是最小单位了,像素的坐标都是整数,从来没有小数坐标。这时候采用的 一种策略就是采用四舍五入的方法(也可以采用直接舍掉小数位的方法),把非整数坐标转换成整数,好,那么按照四舍五入的方法就得到坐标(1,0),完整的 运算过程就是这样的:
(1*0.75,0*0.75)=>(0.75,0)=>(1,0)
那么就可以再填一个象素到目标矩阵中了,同样是把源图中坐标为(1,0)处的像素值38填入目标图中的坐标。
依次填完每个像素,一幅放大后的图像就诞生了,像素矩阵如下所示:
234 38 22 22
67 44 12 12
89 65 63 63
89 65 63 63
这 种放大图像的方法叫做最临近插值算法,这是一种最基本、最简单的图像缩放算法,效果也是最不好的,放大后的图像有很严重的马赛克,缩小后的图像有很严重的 失真;效果不好的根源就是其简单的最临近插值方法引入了严重的图像失真,比如,当由目标图的坐标反推得到的源图的的坐标是一个浮点数的时候,采用了四舍五 入的方法,直接采用了和这个浮点数最接近的象素的值,这种方法是很不科学的,当推得坐标值为 0.75的时候,不应该就简单的取为1,既然是0.75,比1要小0.25 ,比0要大0.75 ,那么目标象素值其实应该根据这个源图中虚拟的点四周的四个真实的点来按照一定的规律计算出来的,这样才能达到更好的缩放效果。双线型内插值算法就是一种 比较好的图像缩放算法,它充分的利用了源图中虚拟点四周的四个真实存在的像素值来共同决定目标图中的一个像素值,因此缩放效果比简单的最邻近插值要好很多。
其他算法
双线性内插值算法描述如下:
对于一个目的像素,设置坐标通过反向变换得到的浮点坐标为(i+u,j+v) (其中i、j均为浮点坐标的整数部分,u、v为浮点坐标的小数部分,是取值[0,1)区间的浮点数),则这个像素得值 f(i+u,j+v) 可由原图像中坐标为 (i,j)、(i+1,j)、(i,j+1)、(i+1,j+1)所对应的周围四个像素的值决定,即:
f(i+u,j+v) = (1-u)(1-v)f(i,j) + (1-u)vf(i,j+1) + u(1-v)f(i+1,j) + uvf(i+1,j+1) 公式1
其中f(i,j)表示源图像(i,j)处的的像素值,以此类推。
比 如,象刚才的例子,假如目标图的象素坐标为(1,1),那么反推得到的对应于源图的坐标是(0.75 , 0.75), 这其实只是一个概念上的虚拟象素,实际在源图中并不存在这样一个象素,那么目标图的象素(1,1)的取值不能够由这个虚拟象素来决定,而只能由源图的这四 个象素共同决定:(0,0)(0,1)(1,0)(1,1),而由于(0.75,0.75)离(1,1)要更近一些,那么(1,1)所起的决定作用更大一 些,这从公式1中的系数uv=0.75×0.75就可以体现出来,而(0.75,0.75)离(0,0)最远,所以(0,0)所起的决定作用就要小一些, 公式中系数为(1-u)(1-v)=0.25×0.25也体现出了这一特点;
后 记:近日在论坛上看到有人提问,说写的图像缩放算法放大图片的时候出现了空缺。分析了一下代码,发现是犯了这样的错误:缩放图片的时候,遍历源图的像素, 然后推出目标图中的对应坐标,进行逐点的像素拷贝,这样当然放大图片的时候会出现空缺。缩小图片的时候结果是对的,但是会出现多余的计算量。
图像缩放算法一定要反推:遍历目标图的像素,反推得到源图中的对应坐标,然后进行像素拷贝。这样才保证了放大图片没有空缺,缩小图片也不会引入冗余计算。
以上内容没有设计到双线性内插值算法计算公式的具体原理,下面简单分析下:
当你制作一个使用地形的游戏时,你需要知道地形确定点的精确高度。例如,在地形上移动一个模型时(见教程4-17),当检测到光标和地形之间的碰撞时(下一个教程),或防止相机与地形碰撞时(见教程2-6)。
因为在前一个教程中你定义了地形每个顶点的3D位置,所以获取这些点的高度很简单。对位于这些顶点间的位置而言,你需要使用一种插值方法获取这个位置的精确高度。
解决方案
如果你想知道高度的点与地形的一个顶点发生碰撞,那么你已经知道了地形上该点的精确高度。如果这个点并没有与顶点发生碰撞,那么说明这个点在地形的一个三角形上。因为三角形是一个平面,所以你可以通过在三角形三个顶点间进行插值获取任何点的精确高度。
工作原理
首先从X和Z坐标开始,你想知道该点的对应Y坐标,这可以通过对三角形三个顶点的高度进行插值获取。
这意味着你首先要找到点究竟在哪个三角形中,这并不容易。但首先我想介绍插值。
线性插值
如果你只处理分离的数据、想知道分离点之间的某些值,需要用到某种类型的插值。这种情况如图5-17坐标所示。对某些分离的(整数) X值,你知道Y值。当X=2,你知道Y=10,X=3时Y=30。但你不知道X=2.7时的Y值。
使用线性插值,你通过连接两点的线段找到X=2.7对应的Y值,如图5-17所示。使用线性插值,通过连接两点的线段找到X=2.7对应的Y值。线 性插值总是将X表达成0和1之间,0对应X的最小值(你知道对应的Y值,本例中为2),1对应X的最大值(本例中为3) 。本例中你想找到X=2.7时的Y值,结果是0.7,意思是“2和3至之间的70%。”
在图5-17的左图中,0%对应Y值10,100%对应Y值20,所以70%对应Y=17。这很容易看出,但右图的情况如何?14对应0.33,因为它是13和16之间的33%。但35和46之间的33%是多少?显然,你希望有代码可以为你计算结果。
首先要有代码找到0和1对应的值。从X值开始,首先减去最小的X值,这样最小值变为0。然后,将最大值缩放为1,你可以通过将它除以最大值和最小值之差实现。
下面是图5-17左图的做法: 2.7→(2.7-min)/(max-min)=(2.7-2)/(3-2)=0.7
然后,进行逆运算获取对应的Y值:首先缩放这个值(通过乘以最大值和最小值的差值),并加到最小的Y值上:
0.7* (maxY-min Y)+minY=0.7*(20-10)+10=0.7*10+10=17
这里你采取图5-17左图简单例子的规则,但你可以使用这个方法计算任何线性插值。看一下图5-17右图更难的例子,在这种情况中,你知道X=13对应Y=35,X=16对应Y=46,但你想知道X=14对应的Y值。所以,首先获取0和1之间对应的值:
14→(14-minX)/(maxX-minX) =(14-13)/(16-13)=0.33
知道了对应值,就做好了获取对应Y值的准备:
0.33* (maxY-minY)+minY=0.33*(46-35)+35=0.33*11+35=3.67+35=38.67
最后,需要进行浮点计算。图5-17的右图中找到X=14对应Y=38.67。事实上,几乎所有插值计算都返回一个浮点数。
技巧
XNA提供了一个功能可以为你计算Vector2, Vector3或Vector4的插值。例如,如果你想获取哪个Vector2位于(5,8)和(2,9)之间的70%,你可以使用Vector2. Lerp(new Vector2(5,8), new Vector2(2,9), 0.7f)。
双线性插值
在地形的例子中,对所有(X,Z)值,你已经定义了一个顶点并知道了它的精确高度。对所有在这些独立顶点之间的(X,Z)值,你不知道精确的Y值,所以需要进行插值。这次你需要获取0和1之间的值,包含X和Z。
有了这些值,就可以分两步计算出精确的Y值。
获取对应值
给定任意(X,Z)坐标,你需要找到地形上的精确Y高度。首先使用前面的公式找到对应的X和Z值,但这次因为在3为空间中,你需要用两次:
int xLower = (int)xCoord; int xHigher = xLower + 1; float xRelative = (xCoord - xLower) / ((float)xHigher - (float)xLower); int zLower = (int)zCoord; int zHigher = zLower + 1; float zRelative = (zCoord - zLower) / ((float)zHigher- (float)zLower); 在地形中每个X和Z的整数值你定义了一个顶点,所以你知道精确的Y值。所以对每个X的浮点数,你要将它们转换为整数获取最小的X值(例如,2.7变 为2)。将这个值加1获取最大X值(2.7对应3作为最大值)。知道了边界,很容易使用前面的公式找到对应值。Z值的求法类似。
获取minY和maxY的值
知道了0和1之间的对应值,下一步是找到精确Y值。但是,首先需要知道minY和maxY值。这些值表示顶点中的高度。你需要知道点在哪个三角形中才能知道使用哪个顶点的高度作为Y值。
你知道点P的X和Z坐标,所以你知道点周围的四个顶点,很容易获取它们的Y值:
float heightLxLz = heightData[xLower, zLower]; float heightLxHz = heightData[xLower, zHigher]; float heightHxLz = heightData[xHigher, zLower]; float heightHxHz = heightData[xHigher, zHigher]; LxHz表示“低X坐标,高Z坐标” 决定(X,Z)。
点在哪个三角形中用来绘制地形的两个三角形。有两个方式可以定义这两个三角形,如图5-18所示。绘制三角形的方式影响到P点的高度,如图所示。
虽然四个顶点有相同的坐标,但两种情况中的点的高度并不相同,图中你可以可出明显的区别。
基于我即将讨论的理由,更好的方式是图5-18的右图。
使用这个旋转方式,很容易确定点在哪个三角形上方。两个三角形之间的边界由对角线给出。在右图中,如果xRelative + zRelative 为1的话,这条线对应具有X和Z坐标的点。
例如,如果这个点在四个点中央,如图5-18所示,xRelative和zRelative都是0.5f,所以和为1,说明在对角线上。如果这个点 偏向左边一点,xRelative会小一些,和会小于1,对Z坐标也是类似的情况。所以如果和小于1,(X,Z)坐标位于左下角的三角形内;否则,该点在 右上角的三角形内:
bool pointAboveLowerTriangle = (xRelative + zRelative < 1);
注意
图5-16中定义的所有三角形都是以图5-18右图中的形式绘制的。
获取精确高度
知道了对应高度,四个周围顶点的高度和点位于哪个三角形中,你就可以计算精确高度了。
如果点在左下方的三角形中,这时pointAboveLowerTriangle为true,下面是使用
双线性插值获取三角形任意点高度的代码:
finalHeight = heightLxLz; finalHeight += zRelative * (heightLxHz - heightLxLz); finalHeight += xRelative * (heightHxLz - heightLxLz); 根据前面解释的单插值的方法,从lowestX的Y值开始。因为这是“双”插值,你要从lowestXlowestZ的Y值开始。
在单插值中,你maxY之间添加高度差,并乘以对应的X值。在双插值中,你乘的是 zRelative和xRelative。
换句话说,从左下顶点的高度开始,对这个高度,你添加了这个顶点和有着更高Z坐标的顶点间的高度差,并乘以距离第二个顶点的Z坐标的接近程度。最后一行代码类似:对这个高度,你添加了左下顶点和右下顶点的高度差,乘以距离右下顶点的X坐标的接近程度。
如果该点在右上三角形的内部,这时pointAboveLowerTriangle为false,情况有所不同,你需要以下代码:
finalHeight = heightHxHz; finalHeight += (1.0f - zDifference) *(heightHxLz - heightHxHz); finalHeight += (1.0f - xDifference) * (heightLxHz - heightHxHz); 从高度开始,从右上顶点开始,遵循同样的步骤:添加高度差,乘以对应距离。
代码
这个方法包含前面解释的所有代码。基于任意(X,Z)坐标,无论是整数还是浮点数,这个方法返回该点的精确高度。首先检查该点是否在地形上。如果不是,返回默认的高度10。
public float GetExactHeightAt(float xCoord, float zCoord) { bool invalid = xCoord < 0; invalid |= zCoord < 0; invalid |= xCoord > heightData.GetLength(0) - 1; invalid |= zCoord > heightData.GetLength(1) - 1; if (invalid) return 10; int xLower = (int)xCoord; int xHigher = xLower + 1; float xRelative = (xCoord - xLower) / ((float)xHigher - (float)xLower); int zLower = (int)zCoord; int zHigher = zLower + 1; float zRelative = (zCoord - zLower) / ((float)zHigher - (float)zLower); float heightLxLz = heightData[xLower, zLower]; float heightLxHz = heightData[xLower, zHigher]; float heightHxLz = heightData[xHigher, zLower]; float heightHxHz = heightData[xHigher, zHigher]; bool pointAboveLowerTriangle = (xRelative + zRelative < 1); float finalHeight; if (pointAboveLowerTriangle ) { finalHeight = heightLxLz; finalHeight += zRelative * (heightLxHz - heightLxLz); finalHeight += xRelative * (heightHxLz - heightLxLz); } else { finalHeight = heightHxHz; finalHeight += (1.0f - zRelative) * (heightHxLz - heightHxHz); finalHeight += (1.0f - xRelative) * (heightLxHz - heightHxHz); } return finalHeight; } 原理
线性插值并不难理解。以图像处理领域为例,我们的理想图像是均匀的分布在二维平面直角坐标系中的,任意给出一对坐标,就应该能够得到一个对应的颜色值,然 而现实是残酷的,我们只能够用离散的点阵信息来近似表现图像。
假设给定一对坐标(2.2, 4.0),想要得到这个坐标对应的颜色,那么比较简单的方法是用四舍五入方法来得到距离该点最近的像素,即像素 (2,
4)的值来代替,这显然并不十分的精确,如果用这个方法进行图像放大,那么在比例较大的情况下就会出现明显的“马赛克”现 象。
对于上面的例子,更好的办法是把像素(2, 4)和像素(3, 4)的值按照一定的比例混合。比例如何选取呢?很简单,离哪个像素近,哪个像素的比例就大些。那么(简单起见,后面均假设是灰度图),若设像素 (2,
4)的值是V_24,像素(3, 4)的值是V_34,就可以得到:
坐标(2.2, 4.0)的颜色值 V(2.2, 4.0) = V_24*(1-0.2)+V_34*0.2
好,你已经懂得什么叫线性插值了!
二次线性插值也就不难理解了。这次我们给的坐标不再是那么体贴了——求坐标(2.2, 4.6)的颜色值。那么可以想到:可以先分别求出坐标(2.2,
4.0)和坐标(2.2, 5.0)的颜色值,然后用一次纵向的线型插值,就得到了:
坐标(2.2, 4.0)的颜色值 V(2.2, 4.0) = V_24*(1-0.2)+V_34*0.2
坐标(2.2, 5.0)的颜色值 V(2.2, 5.0) = V_25*(1-0.2)+V_35*0.2
坐标(2.2, 4.6)的颜色值 = V(2.2, 4.0)*(1-0.6)+V(2.2,
5.0)*0.6
到这里,实际上我们已经得到了二次线性插值的计算公式,表述方便起见下面用符号来表示。
设坐标(x, y)的相邻四个像素值分别为p00, p01, p10, p11, 水平方向的比例系数为h0, h1,
垂直方向的比例系数v0, v1(其中h0+h1=1, v0+v1=1),那么用bilinear interpolation得到:
v(x, y) = (p00*h0+p01*h1)*v0 + (p10*h0+p11*h1)*v1 ................(1.1)
有了这个公式,已经可以编写出算法了,但是这个公式里有六次浮点乘法,如果是真彩图的话,则对每一像素都要有18次浮点乘法!这还不算生成浮点坐标值的时 间(比如在旋转算法当中,每得到一对浮点坐标还要有若干次浮点运算)。
优化
学过一些线性代数知识的朋友可能已经注意到,公式(1.1)其实可以写成矩阵连乘的形式:
|p00 p01| |h0|
v(x, y) = |v0 v1|*| |*| | ................................(1.2)
|p10 p11| |h1|
那么我们就可以利用矩阵相乘的运算法则来优化算法。首先,这里的运算瓶颈是v0, v1, h0, h1这四个浮点值带来的,而实际上我们需要这么高的精度吗?p00,
p01, p10, p11以及我们的运算结果都是整数(对于我们的情况,是0-255之间的整数)。也就是说,其实把我们的结果最后赋值给v(x, y)时,小数部分已经被截掉了,我们根本用不到那么高的精度!那么我们可以尝试用整数乘法代替浮点乘法。
比如,令V0 = (int)(v0*65536.0+0.5),V1 = 65536-V0,H0 = (int)(h0*65536.0+0.5),
H1 = 65536-H0,那么有:
|p00 p01| |H0|
v(x, y)*65536*65536 = |V0 V1|*| |*| | ....................(1.3)
|p10 p11| |H1|
计算出(1.3)式右边的值,左边就可以用右移来代替除法计算出v(x, y)的值。当然实际实现算法的时候,这个公式是一定会溢出的,因为有符号整数的最大值不过是+(32768*65536-1),所以可以在运算中间过程中 就进行移位运算。
当然,优化不能只局限于这个函数,否则是没有意义的,在设计整个算法的时候(即需要用到bilinear interploation的某个图像处理算法),就应该避免使用浮点,保证V0,
V1, H0, H1是整形值。在WannaPlayDIB库中,DIB_RotateFast就是个很好的例子,在循环中央的ox, oy如果不进行右移,就可以通过截取低16位值的方法来得到上面对应的H1和V1,而H0
= 65536-H1, V0 = 65536-V1。因此很容易就能写出DIB_RotateFast的二次插值版本。
< /p>
< /p>
维基百科,自由的百科全 书
双线性插值 ,又称为双线性内插 。在数学 上,双线性插值 是有两个变量的插值 函数的线性插值 扩展,其核心思想是在两个方向分别进行一次线性插值。
红色的数据点与待插值得到的绿色点
假如我们想得到未知函数 f 在点 P = (x , y ) 的值,假设我们已知函数 f 在 Q 11 = (x 1 , y 1 )、Q 12 = (x 1 , y 2 ), Q 21 = (x 2 , y 1 ) 以及 Q 22 = (x 2 , y 2 ) 四个点的值。
首先在 x 方向进行线性插值,得到
然后在 y 方向进行线性插值,得到
这样就得到所要的结果 f (x , y ),
如果选择一个坐标系统使得 f 的四个已知点坐标分别为 (0, 0)、(0, 1)、(1, 0) 和 (1, 1),那么插值公式就可以化简为
或者用矩阵 运算表示为
与这种插值方法名称不同的是,这种插值方法并不是线性的,它的形式是
它是两个线性函数的乘积。另外,插值也可以表示为
在这两种情况下,常数的数目]都对应于给定的 f 的数据点数目。
线性插值的结果与插值的顺序无关。首先进行 y 方向的插值,然后进行 x 方向的插值,所得到的结果是一样的。
双线性插值的一个显然的三维空间延伸是三线性插值 /09/20/4573771.aspx