插值的思想很简单,就是根据周围像素的信息来生成一些像素点,让图像放大之后依然保持真实和平滑。在opencv中,实际上就是直接通过cv2.resize方法中的interpolation参数来实现,这个参数有几种选择:
先讲一下INTER_LINEAR,这个方法一般是用于放大图像。
网上提到双线性插值的原理很多,本身原理也不复杂,但是我自己比较较真,构造了一个简单的python代码,然后通过调试想去真实的对比一下数据,在对比的过程找到一般原理文章中没有的一些细节问题。有兴趣的小伙伴可以详细阅读一下下面的记录。
讲双线性插值前当然要说一下什么叫做线性插值。插值算法一般是用于拉伸图像,也就是说把图像在X或者Y方向上做拉伸,拉伸过后,原来的像素点就不够用了,中间的像素点需要进行补充。那么线性的意思就是中间的这些像素点的像素值就是一个线性关系,在数学中的线性关系就是:
y
=
a
x
+
b
y = ax + b
y=ax+b

假设x0和x1是原图中的两个像素点,那么拉伸之后中间位置多出来的像素点x的像素值就可以用线性的方法来计算:
f
(
x
)
=
x
−
x
0
x
1
−
x
0
∗
f
(
x
1
)
+
x
1
−
x
x
1
−
x
0
∗
f
(
x
0
)
f(x) = \frac{x-x0}{x1-x0}*f(x_1) + \frac{x1-x}{x1-x0}*f(x_0)
f(x)=x1−x0x−x0∗f(x1)+x1−x0x1−x∗f(x0)
简单的来说,就是用x与x0的距离去乘以x1的像素值,用x与x1的距离去乘以x0的像素值,然后再两者相加,这种方式可以保证图像的空间对称性。
因为图像是一个二维结构,那么确定一个新的像素点的像素值就需要X,Y两个方向上的插值,这就叫做双线性插值(其实是做了三次线性插值运算)。
扩展到二维空间的情况如下(图凑合看吧):

如果需要计算坐标(x, y)的像素值的话,通过下面三步运算就可以得到:
获得 (x, y0)的像素值:
f
(
x
,
y
0
)
=
x
−
x
0
x
1
−
x
0
∗
f
(
x
1
,
y
0
)
+
x
1
−
x
x
1
−
x
0
∗
f
(
x
0
,
y
0
)
f(x,y_0) = \frac{x-x_0}{x1-x_0}*f(x_1,y_0) + \frac{x_1-x}{x_1-x_0}*f(x_0,y_0)
f(x,y0)=x1−x0x−x0∗f(x1,y0)+x1−x0x1−x∗f(x0,y0)
获得 (x, y1)的像素值:
f
(
x
,
y
1
)
=
x
−
x
0
x
1
−
x
0
∗
f
(
x
1
,
y
1
)
+
x
1
−
x
x
1
−
x
0
∗
f
(
x
0
,
y
1
)
f(x,y_1) = \frac{x-x0}{x_1-x_0}*f(x_1,y_1) + \frac{x_1-x}{x_1-x_0}*f(x_0,y_1)
f(x,y1)=x1−x0x−x0∗f(x1,y1)+x1−x0x1−x∗f(x0,y1)
然后再在Y方向上做一次插值:
f
(
x
,
y
)
=
y
−
y
0
y
1
−
y
0
∗
f
(
x
,
y
1
)
+
y
1
−
y
y
1
−
y
0
∗
f
(
x
,
y
0
)
f(x,y) = \frac{y-y0}{y_1-y_0}*f(x,y_1) + \frac{y_1-y}{y_1-y_0}*f(x,y_0)
f(x,y)=y1−y0y−y0∗f(x,y1)+y1−y0y1−y∗f(x,y0)
这样,就可以得到这个像素值了。
上面的计算方式已经说过了,那么还剩一个问题,如何映射原图和目标图的坐标,因为上面公式中提到的所有的像素值都只能是原图中才有。
假设我们是将原图放大三倍(X和Y方向都是),那么对于每一个目标图像的像素点坐标(x_dst, y_dst)都需要映射到原图中。对于每一个坐标(x_dst, y_dst),对应的原图坐标(x_src, y_src)可以通过下面的公式计算得到:
s
r
c
X
=
d
s
t
X
(
s
r
c
w
i
d
t
h
/
d
s
t
w
i
d
t
h
)
srcX = dstX(src_{width}/dst_{width})
srcX=dstX(srcwidth/dstwidth)
s r c Y = d s t Y ( s r c h e i g h t / d s t h e i g h t ) srcY = dstY(src_{height}/dst_{height}) srcY=dstY(srcheight/dstheight)
在我们的例子中,这个计算出来肯定是一个小数,比如目标图中的(4, 4)这个像素点,计算下来得到的结果是(4/3, 4/3),那么怎么去做上面的计算呢?也就是上面图中的(x0, y0)和(x1, y1)怎么挑选的问题。很容易想到,那就把X_dst和Y_dst向下和向上取整,就可以得到四个像素点,就可以形成上图中的四个像素,在这个例子中,这四个像素点就是:
这样所有的值都是已知的,就可以把像素值计算出来了。
在opencv中,上面的位置变换公式中还会添加一个调节因子:
0.5
(
s
r
c
w
i
d
t
h
/
d
s
t
w
i
d
t
h
−
1
)
0.5(src_{width}/dst_{width} - 1)
0.5(srcwidth/dstwidth−1)
也就是改成:
s
r
c
X
=
d
s
t
X
(
s
r
c
w
i
d
t
h
/
d
s
t
w
i
d
t
h
)
+
0.5
(
s
r
c
w
i
d
t
h
/
d
s
t
w
i
d
t
h
−
1
)
srcX = dstX(src_{width}/dst_{width}) + 0.5(src_{width}/dst_{width} - 1)
srcX=dstX(srcwidth/dstwidth)+0.5(srcwidth/dstwidth−1)
s r c Y = d s t Y ( s r c h e i g h t / d s t h e i g h t ) + 0.5 ( s r c w i d t h / d s t w i d t h − 1 ) srcY = dstY(src_{height}/dst_{height}) +0.5(src_{width}/dst_{width} - 1) srcY=dstY(srcheight/dstheight)+0.5(srcwidth/dstwidth−1)
那么上面说的(4, 4)这个位置,对应到原图中的位置就是(1, 1)。
接上一点,这里会有一个细节问题,对于映射到原图中的坐标(1, 1),没法向下和向上取整,只有一个坐标,没法搞出4个坐标来,这里我通过实验测试,opencv对于这种情况应该是向后去扩展,也就是把四个坐标变成:
有兴趣的小伙伴可以去翻一下源码,有结果了可以和我交流。
还有一个细节问题是,目标图中的边缘像素点,通过调整因子可能会算成负数。比如目标图中的( 0, 3),通过映射之后得到的结果是(-1/3, 2/3),很明显。图像中不可能存在负数,那么这种情况下就直接使用(0, 0) (0, 1) (1, 0) (1,1)四个点来进行计算。
如果是四个顶点,直接采用原图的四个顶点作为新图的顶点像素值。
通过构造一个10 * 10的黑底图像,在这个图像上画一个2 * 2的矩形框,然后放大三倍。
代码如下:
img = np.zeros((10, 10, 3), dtype="uint8")
cv2.rectangle(img, (2, 2), (7, 7), (255, 255, 255), 1)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
dst_size = (round(img.shape[0] * 3.0), round(img.shape[1] * 3.0))
img_large = cv2.resize(img, dst_size, 3.0, 3.0, interpolation=cv2.INTER_LINEAR)
cv2.imshow("img_re", img)
cv2.imshow("img_large", img_large)
cv2.waitKey(0)
cv2.destroyAllWindows()
通过调试模式,找几个像素值来验证上面的计算逻辑:

红框中的数字的坐标是:(5,5)(从0开始),可以代入到上面的公式中,计算原图映射坐标点:
x
=
5
∗
100
/
300
+
0.5
∗
(
100
/
300
−
1
)
=
4
/
3
x = 5 * 100 / 300 + 0.5 * (100 / 300 -1) = 4/3
x=5∗100/300+0.5∗(100/300−1)=4/3
y = 5 ∗ 100 / 300 + 0.5 ∗ ( 100 / 300 − 1 ) = 4 / 3 y = 5 * 100 / 300 + 0.5 * (100 / 300 -1) = 4/3 y=5∗100/300+0.5∗(100/300−1)=4/3
对应的原图的四个点就是:(1,1) (2,1) (1,2) (2,2)
对应的原图数据就是:

做三次插值运算(建议看到这里的小伙伴拿笔一个一个值在纸上代入计算一下):
f
(
x
,
1
)
=
(
4
/
3
−
1
)
∗
0
+
(
2
−
4
/
3
)
∗
0
=
0
f(x, 1) = (4/3 - 1) * 0 + (2 - 4/3) * 0 = 0
f(x,1)=(4/3−1)∗0+(2−4/3)∗0=0
f ( x , 2 ) = ( 4 / 3 − 1 ) ∗ 255 + ( 2 − 4 / 3 ) ∗ 0 = 85 f(x, 2) = (4/3 - 1) * 255 + (2 - 4/3) * 0 = 85 f(x,2)=(4/3−1)∗255+(2−4/3)∗0=85
f ( x , y ) = ( 4 / 3 − 1 ) ∗ 85 + ( 2 − 4 / 3 ) ∗ 0 = 28 f(x, y) = (4/3 - 1) * 85 + (2 - 4/3) * 0 = 28 f(x,y)=(4/3−1)∗85+(2−4/3)∗0=28
符合上面描述的逻辑。

这个数据通过坐标映射后是(2, 2),也可以通过上面的逻辑计算得到255。
在opencv4.5中,上面的双线性插值是默认选项。如果选择INTER_NEAREST作为参数的话,就是最邻值插值算法。这个算法相对比较简单,就是通过上面提到的坐标映射之后,然后确定周边的四个像素点,计算这个坐标与这四个像素点的坐标距离,选择最近的那个坐标的像素点的值作为目标图像的像素值。
距离的计算公式就是:
d
=
(
x
s
r
c
−
x
)
2
+
(
y
s
r
c
−
y
)
2
d = (x_{src}-x)^2 + (y_{src}-y)^2
d=(xsrc−x)2+(ysrc−y)2
这样有一个问题就是很容易形成马赛克,毕竟这样的方式过于简单。
这一篇内容足够了,下一篇准备中。
在图像处理中,分辨率这个概念在绝大多数场景中都会用到,但其实不是同一个意思,我自己总结其实有两个意思:
这两个概念都可以拿来描述图像的清晰程度,但是用的场景不一样。
先不直接说这两个概念的意义,先来想一下,一幅数字图像实际上外界的真实的光在数码相机的感光板,也就是CCD上生成的。CCD是由一个一个的感光元器件组成的,每个元器件就可以形成一个像素的数据。那么这里就有了一个问题:现实世界里真实的事物是连续的,那么怎么把现实世界里连续的数据变成离散的一个一个的像素点呢?这就是采样了。
假设一个固定大小的CCD感光板(M * N)能感受到X米 * Y米的真实世界大小,那么就是把X米 * Y米的真实世界划分成了M * N个网格,一个像素点的数据就用来表现这 X/M * Y/N框的真实世界了。
所以,图像的分辨率应该这么去理解:
显示器的分辨率是由像素点组成的,现在的显示器都是液晶显示器,就是由分辨率个led的显示元器件组成(不一定严谨,大概是这么个意思)。
而DPI一般是用于打印,比如在一英寸里打印300个像素点和一英寸中打印100个像素点,清晰度肯定是不一样的。
光学变焦就是是通过调整镜头的焦距来改变CCD的感光区域,CCD还是获得真实区域的光,只是感受到的真实世界区域变小了,或者说,相机用更多的像素点去描述某一块区域,自然更加清晰。
还会出现在一个场景,就是在显示器上现实一个图像的时候,比如这个图像是由3000 * 2000的CCD拍摄出来的,那么这幅图像实际就是由3000 * 2000 ,共600w像素组成。假设显示器实际上的分辨率是1500 * 1000。那么肯定是无法将这幅图像完整的显示出来的,那么就只有两个选择:
或者我们反过来,一副200 * 200的图像要在400 * 400的屏幕上进行显示,那么这多出来的像素点应该是什么像素值呢?这就是“上采样”要做的事情,也就是进行插值计算。