插值

​ 插值的思想很简单,就是根据周围像素的信息来生成一些像素点,让图像放大之后依然保持真实和平滑。在opencv中,实际上就是直接通过cv2.resize方法中的interpolation参数来实现,这个参数有几种选择:

  • INTER_LINEAR,bilinear interpolation,双线性插值法,为默认参数。
  • INTER_CUBIC

​ 先讲一下INTER_LINEAR,这个方法一般是用于放大图像。

双线性插值

​ 网上提到双线性插值的原理很多,本身原理也不复杂,但是我自己比较较真,构造了一个简单的python代码,然后通过调试想去真实的对比一下数据,在对比的过程找到一般原理文章中没有的一些细节问题。有兴趣的小伙伴可以详细阅读一下下面的记录。

线性插值

​ 讲双线性插值前当然要说一下什么叫做线性插值。插值算法一般是用于拉伸图像,也就是说把图像在X或者Y方向上做拉伸,拉伸过后,原来的像素点就不够用了,中间的像素点需要进行补充。那么线性的意思就是中间的这些像素点的像素值就是一个线性关系,在数学中的线性关系就是:
y = a x + b y = ax + b y=ax+b
python的opencv操作记录(五) - 插值第一篇-LMLPHP

​ 假设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)=x1x0xx0f(x1)+x1x0x1xf(x0)
​ 简单的来说,就是用x与x0的距离去乘以x1的像素值,用x与x1的距离去乘以x0的像素值,然后再两者相加,这种方式可以保证图像的空间对称性。

双线性插值

​ 因为图像是一个二维结构,那么确定一个新的像素点的像素值就需要X,Y两个方向上的插值,这就叫做双线性插值(其实是做了三次线性插值运算)。

​ 扩展到二维空间的情况如下(图凑合看吧):

python的opencv操作记录(五) - 插值第一篇-LMLPHP

​ 如果需要计算坐标(x, y)的像素值的话,通过下面三步运算就可以得到:

  1. 获得 (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)=x1x0xx0f(x1,y0)+x1x0x1xf(x0,y0)

  2. 获得 (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)=x1x0xx0f(x1,y1)+x1x0x1xf(x0,y1)

  3. 然后再在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)=y1y0yy0f(x,y1)+y1y0y1yf(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向下和向上取整,就可以得到四个像素点,就可以形成上图中的四个像素,在这个例子中,这四个像素点就是:

    • (1, 1)
    • (2, 1)
    • (1, 2)
    • (2, 2)

    这样所有的值都是已知的,就可以把像素值计算出来了。

  • 在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/dstwidth1)
    也就是改成:
    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/dstwidth1)

    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/dstwidth1)

    那么上面说的(4, 4)这个位置,对应到原图中的位置就是(1, 1)。

  • 接上一点,这里会有一个细节问题,对于映射到原图中的坐标(1, 1),没法向下和向上取整,只有一个坐标,没法搞出4个坐标来,这里我通过实验测试,opencv对于这种情况应该是向后去扩展,也就是把四个坐标变成:

    • (1, 1)
    • (2, 1)
    • (1, 2)
    • (2, 2)

    有兴趣的小伙伴可以去翻一下源码,有结果了可以和我交流。

  • 还有一个细节问题是,目标图中的边缘像素点,通过调整因子可能会算成负数。比如目标图中的( 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()

​ 通过调试模式,找几个像素值来验证上面的计算逻辑:

  • 先看目标图的数据:

python的opencv操作记录(五) - 插值第一篇-LMLPHP

​ 红框中的数字的坐标是:(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=5100/300+0.5(100/3001)=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=5100/300+0.5(100/3001)=4/3

  • 对应的原图的四个点就是:(1,1) (2,1) (1,2) (2,2)

    对应的原图数据就是:

python的opencv操作记录(五) - 插值第一篇-LMLPHP

​ 做三次插值运算(建议看到这里的小伙伴拿笔一个一个值在纸上代入计算一下):
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/31)0+(24/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/31)255+(24/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/31)85+(24/3)0=28

​ 符合上面描述的逻辑。

python的opencv操作记录(五) - 插值第一篇-LMLPHP

这个数据通过坐标映射后是(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=(xsrcx)2+(ysrcy)2

​ 这样有一个问题就是很容易形成马赛克,毕竟这样的方式过于简单。

三次样条插值

​ 这一篇内容足够了,下一篇准备中。

图像分辨率的一点小问题

​ 在图像处理中,分辨率这个概念在绝大多数场景中都会用到,但其实不是同一个意思,我自己总结其实有两个意思:

  • 第一种分辨率,可以说是DPI。
  • 第二种分辨率,实际上是图像的尺寸,或者说是像素点的个数。

​ 这两个概念都可以拿来描述图像的清晰程度,但是用的场景不一样。

​ 先不直接说这两个概念的意义,先来想一下,一幅数字图像实际上外界的真实的光在数码相机的感光板,也就是CCD上生成的。CCD是由一个一个的感光元器件组成的,每个元器件就可以形成一个像素的数据。那么这里就有了一个问题:现实世界里真实的事物是连续的,那么怎么把现实世界里连续的数据变成离散的一个一个的像素点呢?这就是采样了。
​ 假设一个固定大小的CCD感光板(M * N)能感受到X米 * Y米的真实世界大小,那么就是把X米 * Y米的真实世界划分成了M * N个网格,一个像素点的数据就用来表现这 X/M * Y/N框的真实世界了。
​ 所以,图像的分辨率应该这么去理解:

  1. 我们平时说的1600*1200说的是这个图像由多少个像素组成。和真实的图像清晰度没有太大的关系,因为根据上面的说法,一个像素表达了多大范围的真实物体,是根据采样率来的。
  2. 假设采样率很高,也就是一个像素代表较少范围的一个区域。可以想象,这个图像的表现真实世界的细节是更丰富的,那么这个东西就是我们说的DPI了,每英寸像素数,dots per inch。
  3. 所以说,一个图像是否清晰,是要这两个方面一起来决定的。

​ 显示器的分辨率是由像素点组成的,现在的显示器都是液晶显示器,就是由分辨率个led的显示元器件组成(不一定严谨,大概是这么个意思)。
​ 而DPI一般是用于打印,比如在一英寸里打印300个像素点和一英寸中打印100个像素点,清晰度肯定是不一样的。

光学变焦 & 数码变焦

​ 光学变焦就是是通过调整镜头的焦距来改变CCD的感光区域,CCD还是获得真实区域的光,只是感受到的真实世界区域变小了,或者说,相机用更多的像素点去描述某一块区域,自然更加清晰。

显示器显示逻辑

​ 还会出现在一个场景,就是在显示器上现实一个图像的时候,比如这个图像是由3000 * 2000的CCD拍摄出来的,那么这幅图像实际就是由3000 * 2000 ,共600w像素组成。假设显示器实际上的分辨率是1500 * 1000。那么肯定是无法将这幅图像完整的显示出来的,那么就只有两个选择:

  1. 截取图像中1500 * 1000个像素的矩形框,也就是1/4幅图像进行显示
  2. 如果显示全部的话,就只能从两个像素中选择一个进行显示,也就是进行“降采样”,或者说是“下采样”

或者我们反过来,一副200 * 200的图像要在400 * 400的屏幕上进行显示,那么这多出来的像素点应该是什么像素值呢?这就是“上采样”要做的事情,也就是进行插值计算。

06-24 10:48