目录
- 前言
- 最近邻插值
- 理论与公式部分
- 代码部分
- 优缺点
- 双线性插值
- 理论与公式部分
- 代码实现
- 优缺点
- 双三次内插
前言
最近邻插值和双线性插值是两种常见的用于图像处理的方法,主要是用于实现图像的放大和缩小。本文中将以最为简单粗暴的方式介绍两种方法的原理,以及底层的代码实现。
为什么需要有插值的存在呢?这是因为计算机中存储的数据都是离散的。假设有一个350×350的图片,现在我要将其放大两倍,即新图为750×750,。对于原图当中的每一个像素,可以将其像素值的坐标对应乘上2,将其映射到新图中(如对于点(150, 100),其在新图中的坐标为(300, 200))。
但是这样做就有一个问题,新图中的像素点显然是更多的,这就会导致很多点并不能在原图中找到完全对应的点,这是因为原图中的坐标是离散分布的(如新图中的(201, 101),按照映射比例应该为(100.5, 50.5),但这在原图中是不存在的)。因此就会导致像素点的丢失。所以我们就需要采用一些办法解决这个问题。
最近邻插值
理论与公式部分
最近邻插值是最好理解也是最好实现的一种方法。其基本思想是**在目标像素点上插入离对应原像素点最近的点的像素值,而不考虑图像可能的颜色变化。**这是什么意思呢?我们仍然根据前言中的例子分析。
对于目标图中的点(300, 200),其通过逆变换得到的值是真实存在于原图中的,此时不需要做处理,直接将原图点像素值给到目标点即可;而对于目标图中的点(201, 101),根据映射关系对其进行逆变换得到其在原图上的点是(100.5, 50.5),但是原图中并不存在这个像素点,不过可以根据四舍五入的规则,找到原图真实存在的像素点中,与目标点距离最近的点。最近邻插值的办法就是,选取与目标点距离最近的一个点,并将该点的像素值赋给目标点。 那么对于(100.5, 50.5),按照四舍五入的规则,其像素值就应该同原图中点(101, 51)一致。
根据以上思路,总结为数学公式如下:
s
r
c
x
=
d
s
t
x
∗
d
s
i
z
e
x
src_x=dst_x*dsize_x
srcx=dstx∗dsizex
s
r
c
y
=
d
s
t
y
∗
d
s
i
z
e
y
src_y=dst_y*dsize_y
srcy=dsty∗dsizey
其中,
s
r
c
x
src_x
srcx、
s
r
c
y
src_y
srcy是原图中用于确定目标点像素值的坐标,
d
s
t
x
dst_x
dstx、
d
s
t
y
dst_y
dsty是目标图中目标点的坐标,dsize分别是提前预设的水平、竖直两个方向的缩放比率。
上述思路是纯粹的数学思路,还没有考虑离散化的问题。实际的计算公式应该增加舍入思想:
s r c x = r o u n d ( d s t x ∗ d s i z e x ) src_x=round(dst_x*dsize_x) srcx=round(dstx∗dsizex) s r c y = r o u n d ( d s t y ∗ d s i z e y ) src_y=round(dst_y*dsize_y) srcy=round(dsty∗dsizey)
代码部分
以下是代码实现:
# 设置缩放比率,依次为水平、竖直比率
dsize = (2, 2)
# 最近邻插值
def nearest_neighbor_interpolation(src_img, dsize):
# 获取原图的宽高属性并计算目标图片的宽高
height, width = src_img.shape[:2]
dst_height = math.ceil(dsize[1] * height)
dst_width = math.ceil(dsize[0] * width)
# 创建新图
dst_img = np.zeros((dst_height, dst_width, 3), dtype=np.uint8)
# 遍历处理
for r in range(dst_height):
for c in range(dst_width):
# 根据最近点取值
src_r, src_c = round(r / dsize[1]), round(c / dsize[0])
# 防溢出处理
if src_r >= height:
src_r = height - 1
if src_c >= width:
src_c = width - 1
# 插值
dst_img[r, c] = img[src_r, src_c]
# 返回处理后图像
return dst_img
优缺点
最近邻插值的优点是思路简单,实现也很简单。但缺点就是确定目标点像素值时考虑因素太少,处理后的图片失真比较大,马赛克现象严重。这也就引入了接下来要说的双线性插值办法。
双线性插值
理论与公式部分
根据上述的分析不难知道,最近邻插值方法中,对于目标点像素值的选取仅仅采用与其最近的那个点作为参考,效果并不是很好。而双线性插值中对于目标点像素值的选取,综合考虑了离该点在原图中的映射点最近的4个点。
我们已经知道,对一个像素值待确定的目标点,按照原图和目标图像的映射关系计算,计算的结果肯定是浮点类型,但是图像的像素点索引必须是整数,所以这里求出的坐标可以认为是虚拟坐标,也就是不存在的。但可以确定的是,该点一定可以被四个确定的点围成的矩形框柱。这样就可以根据这四个已知点更加精细的计算出目标点的像素值。
具体的计算思路是什么呢?其实不过是最基本的方法,每个人初中都学过的知识,那就是直线方程。来看下面这张图,我们知道一条直线上任取两个点,这两个点的坐标之间一定满足某种恒定关系,这个关系就是斜率。
图中显然存在关系
y
1
−
y
0
x
1
−
x
0
=
y
−
y
0
x
−
x
0
{{y_1-y_0}\over{x_1-x_0}} = {{y-y_0}\over{x-x_0}}
x1−x0y1−y0=x−x0y−y0
整理一下得到:
y
=
x
1
−
x
x
1
−
x
0
y
0
+
x
−
x
0
x
1
−
x
0
y
1
y={{x_1-x}\over{x_1-x_0}}y_0+{{x-x_0}\over{x_1-x_0}}y_1
y=x1−x0x1−xy0+x1−x0x−x0y1这个式子告诉我们:在一条直线上,可以通过两个已知点的坐标就可以直接求出我们想要的任何点的坐标。
由一维的线性插值很容易拓展到二维图像的双线性插值,每次需要要经过三次一阶线性插值才能获得最终结果。
上图是一个二维双线性插值的定量俯视示意图 (点位稍有变动但不影响),我们换个顺序。先由像素坐标点 (x0, y0) 和 (x1, y0) 在 x 轴向作一维线性插值得到 f(x, y0)、由像素坐标点 (x0, y1) 和 (x1, y1) 在 x 轴向作一维线性插值得到 f(x, y1):
f
(
x
,
y
0
)
=
x
1
−
x
x
1
−
x
0
f
(
x
0
,
y
0
)
+
x
−
x
0
x
1
−
x
0
f
(
x
1
,
y
0
)
f(x,y_0)={{x_1-x}\over{x_1-x_0}}f(x_0,y_0)+{{x-x_0}\over{x_1-x_0}}f(x_1,y_0)
f(x,y0)=x1−x0x1−xf(x0,y0)+x1−x0x−x0f(x1,y0)
f
(
x
,
y
1
)
=
x
1
−
x
x
1
−
x
0
f
(
x
0
,
y
1
)
+
x
−
x
0
x
1
−
x
0
f
(
x
1
,
y
1
)
f(x,y_1)={{x_1-x}\over{x_1-x_0}}f(x_0,y_1)+{{x-x_0}\over{x_1-x_0}}f(x_1,y_1)
f(x,y1)=x1−x0x1−xf(x0,y1)+x1−x0x−x0f(x1,y1)
然后再由 (x, y0) 和 (x, y1) 在 y 轴向作一维线性插值得到插值点 (x, y) 的灰度值 f(x, y):
f
(
x
,
y
)
=
y
1
−
y
y
1
−
y
0
f
(
x
,
y
0
)
+
y
−
y
0
y
1
−
y
0
f
(
x
,
y
1
)
f(x,y)={{y_1-y}\over{y_1-y_0}}f(x,y_0)+{{y-y_0}\over{y_1-y_0}}f(x,y_1)
f(x,y)=y1−y0y1−yf(x,y0)+y1−y0y−y0f(x,y1)
联立以上式子,就可以得到最终的表达式:
实际中,我们使用双线性插值的时候,在计算原图坐标时,如果单纯采用同最近邻插值一样的办法,会导致两幅图的几何中心并不重合。为了解决这个办法,可以将坐标计算公式修改为如下形式:
s
r
c
x
=
(
d
s
t
x
+
0.5
)
∗
d
s
i
z
e
x
−
0.5
src_x=(dst_x+0.5)*dsize_x-0.5
srcx=(dstx+0.5)∗dsizex−0.5
s
r
c
y
=
(
d
s
t
y
+
0.5
)
∗
d
s
i
z
e
y
−
0.5
src_y=(dst_y+0.5)*dsize_y-0.5
srcy=(dsty+0.5)∗dsizey−0.5
代码实现
# 双线性插值
def bilinear_interpolation(src_img, dsize):
# 获取原图的宽高属性并计算目标图片的宽高
height, width = src_img.shape[:2]
dst_height = math.ceil(dsize[1] * height)
dst_width = math.ceil(dsize[0] * width)
# 创建新图
dst_img = np.zeros((dst_height, dst_width, 3), dtype=np.uint8)
# 遍历处理
for r in range(dst_height):
for c in range(dst_width):
# 计算原图中映射点的坐标
src_r = (r + 0.5) * height / dst_height - 0.5
src_c = (c + 0.5) * width / dst_width - 0.5
# 获取坐标整数部分
i, j = int(src_r), int(src_c)
# 检查边界
if i >= height - 1:
i = height - 2
if j >= width - 1:
j = width - 2
# 获取坐标浮点数部分
u, v = src_r - i, src_c - j
# 根据双线性插值法原理进行插值
value = (src_img[i, j] * (1 - u) * (1 - v) +
src_img[i, j + 1] * (1 - u) * v +
src_img[i + 1, j] * u * (1 - v) +
src_img[i + 1, j + 1] * u * v)
# 防溢出处理
value = np.clip(value, 0, 255)
dst_img[r, c] = value.astype(np.uint8)
# 返回处理后图像
return dst_img
优缺点
双线性插值法相比于最近邻插值,处理效果会更好,但是计算量会更大。不过如果底层进行一些计算优化的话,效率损耗几乎是可以忽略的。经过实测,本文中实现的双线性插值办法与opencv库中的resize方法处理图像效果非常类似,推测resize底层采用的方法就是双线性插值。不过自己写的方法效率方面慢了很多,应该是resize底层采取了很多加速的优化策略。
双三次内插
双三次内插是一种处理效果更好,但是复杂度更高的算法。它计算时考虑的最近邻点更多,高达16个点,这就使其在保留细节方面更加强大,但是效率也更慢。一般比较高端的商用图像处理软件如Photoshop会采用该方法作为标准内插方法。由于思路和实现较为复杂,感兴趣的读者可以自行查阅相关资料。