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


    插值

    ​ 插值的思想很简单,就是根据周围像素的信息来生成一些像素点,让图像放大之后依然保持真实和平滑。在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
    在这里插入图片描述

    ​ 假设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两个方向上的插值,这就叫做双线性插值(其实是做了三次线性插值运算)。

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

    在这里插入图片描述

    ​ 如果需要计算坐标(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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    • 先看目标图的数据:

    在这里插入图片描述

    ​ 红框中的数字的坐标是:(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)

      对应的原图数据就是:

    在这里插入图片描述

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

    ​ 符合上面描述的逻辑。

    在这里插入图片描述

    这个数据通过坐标映射后是(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的屏幕上进行显示,那么这多出来的像素点应该是什么像素值呢?这就是“上采样”要做的事情,也就是进行插值计算。

  • 相关阅读:
    2023年香港优才计划申请确实火爆,但请冷静!结合个人条件再考虑!
    阿里云大学Apache Flink大数据学习笔记
    事件对象(Event对象)
    【LeetCode】20. 有效的括号
    故障007:dexp导数莫名中断
    Unity开发者——编辑器技巧
    这个 MySQL 问题困扰了我一个月,现在终于把他解决了
    Mybatis总结--传参二
    自定义View
    FPGA杂记
  • 原文地址:https://blog.csdn.net/pcgamer/article/details/125426351