Bootstrap

图像插值:理论与Python实现

前言

在这里插入图片描述

参考这篇论文:《Deep Learning for Image Super-resolution:A Survey》
简单来说,插值指利用已知的点来“猜”未知的点,图像领域插值常用在修改图像尺寸的过程,由旧的图像矩阵中的点计算新图像矩阵中的点并插入,不同的计算过程就是不同的插值算法。
插值算法有很多种,这里列出关联比较密切的三种:

  • 最近邻法(Nearest Interpolation):计算速度最快,但是效果最差。
  • 双线性插值(Bilinear Interpolation):双线性插值是用原图像中4(2*2)个点计算新图像中1个点,效果略逊于双三次插值,速度比双三次插值快,属于一种平衡美,在很多框架中属于默认算法。
  • 双三次插值(Bicubic interpolation):双三次插值是用原图像中16(4*4)个点计算新图像中1个点,效果比较好,但是计算代价过大。

最近邻插值法

(1)理论

最近邻插值法nearest_neighbor是最简单的灰度值插值。也称作零阶插值,就是令变换后像素的灰度值等于距它最近的输入像素的灰度值。
最近邻插值法坐标变换计算公式:
  srcX=dstX*(srcWidth/dstWidth)
  srcY=dstY*(srcHeight/dstHeight)
上式中,dstX与dstY为目标图像的某个像素的横纵坐标,dstWidth与dstHeight为目标图像的长与宽;srcWidth与srcHeight为原图像的宽度与高度。srcX,srcY为目标图像在该点(dstX,dstY)对应的原图像的坐标。与opencv一样,以左上角为(0,0)坐标。
在这里插入图片描述

右图为经过放大后的目标图像,?处的坐标为(3,2),根据公式计算得到
srcX=3*(2/4)=1.5,srcY=2*(2/4)=1;
故?处的像素应该为原图像中的(1.5,1)像素的值,但是像素坐标没有小数,一般采用四舍五入取最邻,所以最终的结果为(2,1),对应原图像的橙色。
在这里插入图片描述

(2)python实现

'''@author: songcongcong'''

import numpy as np
import cv2

# 最近邻插值
def nearest_neighbour(src, dst_shape):
    # 获取原图维度
    src_height, src_width = src.shape[0], src.shape[1]
    # 计算新图维度
    dst_height, dst_width, channels = dst_shape[0], dst_shape[1], dst_shape[2]

    dst = np.zeros(shape = (dst_height, dst_width, channels), dtype=np.uint8)
    for dst_x in range(dst_height):
        for dst_y in range(dst_width):
            # 寻找源图像对应坐标
            src_x = dst_x * (src_width/dst_width)
            src_y = dst_y * (src_height/dst_height)
            
            # 四舍五入会超出索引,这里采用向下取整,也就是原本1.5->2, 现在是1.5->1             
            src_x = int(src_x)             
            src_y = int(src_y)

            # 插值
            ddst[dst_x, dst_y,:] = src[src_x, src_y, :]
    return dst

src = cv2.imread('me.jpg')
dst = nearest_neighbour(src, dst_shape=(720, 540, 3))

# 显示 
cv2.namedWindow('src', cv2.WINDOW_NORMAL) 
cv2.imshow("src",src) 
cv2.namedWindow('dst', cv2.WINDOW_NORMAL) 
cv2.imshow("dst",dst)
cv2.waitKey(0)

双线性插值

在看双线性插值前,先看看单线性插值是怎么实现的,这样才方便理解双线性插值。

(1)单线性插值

已知图中P1点和P2点,坐标分别为(x1, y1)、(x2, y2),要计算 [x1, x2] 区间内某一位置 x 在直线上的y值
在这里插入图片描述
根据初中的知识,2点求一条直线公式(这是双线性插值所需要的唯一的基础公式)
在这里插入图片描述
经过简单整理成下面的格式:在这里插入图片描述
y1与y2分别代表原图像中的像素值,上面的公式可以写成如下形式:在这里插入图片描述

(2)双线性插值

已知Q11(x1,y1)、Q12(x1,y2)、Q21(x2,y1)、Q22(x2,y2),求其中点P(x,y)的值。
在这里插入图片描述

双线性插值是分别在两个方向计算了共3次单线性插值,如图所示,先在x方向求2次单线性插值,获得R1(x, y1)、R2(x, y2)两个临时点,再在y方向计算1次单线性插值得出P(x, y)(实际上调换2次轴的方向先y后x也是一样的结果)。
(1)x方向单线性插值 直接带入前一步单线性插值最后的公式在这里插入图片描述

(2)y方向单线性插值在这里插入图片描述

将第一步结果带入第二步
在对图像做双线性插值时,不难发现,在计算中有这样的关系:在这里插入图片描述

那么上面的公式中的分母全都为1,如下:在这里插入图片描述

遗留问题:源图像和目标图像的原点(0,0)均选择左上角,然后根据插值公式计算目标图像每点像素,假设你需要将一幅5x5的图像缩小成3x3,那么源图像和目标图像各个像素之间的对应关系如下:在这里插入图片描述

只画了一行,用做示意,从图中可以很明显的看到,如果选择右上角为原点(0,0),那么最右边和最下边的像素实际上并没有参与计算,而且目标图像的每个像素点计算出的灰度值也相对于源图像偏左偏上。
那么,让坐标加1或者选择右下角为原点怎么样呢?很不幸,还是一样的效果,不过这次得到的图像将偏右偏下。
最好的方法就是,两个图像的几何中心重合,并且目标图像的每个像素之间都是等间隔的,并且都和两边有一定的边距,这也是matlab和openCV的做法。如下图:在这里插入图片描述

几何中心问题的优化算法如下:在这里插入图片描述

(3)计算过程

写了一堆公式是不是还不知道具体怎么求,要求的就是x和y,但是表达式里就有x和y,那么怎么求解呢?在图像处理的时候,我们先根据 
src_x=(dst_x+0.5) * (src_width/dst_width) - 0.5
src_y = (dst_y+0.5) * (src_height/dst_height) - 0.5
来计算目标像素在源图像中的位置,这里计算的srcX和srcY一般都是浮点数,比如f(1.2, 3.4)这个像素点是虚拟存在的,先找到与它临近的四个实际存在的像素点
  (1,3) (2,3)
  (1,4) (2,4)
写成f(i+u,j+v)的形式,则u=0.2,v=0.4, i=1, j=3
在沿着X方向差插值时,f(R1)=u(f(Q21)-f(Q11))+f(Q11)
沿着Y方向同理计算。或者,直接整理一步计算,
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)
贴个手写计算过程:
在这里插入图片描述

(4)python实现

'''@author: songcongcong'''

# 双线性插值
def bilinear_interpolation(src, dst_shape):
    # 获取原图维度
    src_height, src_width = src.shape[0], src.shape[1]
    # 计算新图维度
    dst_height, dst_width, channels = dst_shape[0], dst_shape[1], dst_shape[2]
    
    dst = np.zeros(shape = (dst_height, dst_width, channels), dtype=np.uint8)
    for dst_x in range(dst_height):
        for dst_y in range(dst_width):
            # 寻找源图像对应坐标
            src_x = (dst_x+0.5) * (src_width/dst_width) - 0.5
            src_y = (dst_y+0.5) * (src_width/dst_width) - 0.5
            
            # 计算插值
            i, j = int(src_x), int(src_y)
            u, v = src_x - i, src_y - j
            f = (1-u)*(1-v)*src[i,j] + (1-u)*v*src[i,j+1] + u*(1-v)*src[i+1,j] + u*v*src[i+1,j+1]
            f = np.clip(f, 0, 255)   # 处理一下越界的数据

            # 插值
            dst[dst_x, dst_y,:] = f.astype(np.uint8)
    return dst

双三次插值

(1)理论

双三次插值也被称为三线性插值,立方卷积插值,三次卷积插值等。
该算法利用待采样点周围16个点的灰度值作三次插值,不仅考虑到4 个直接相邻点的灰度影响,而且考虑到各邻点间灰度值变化率的影响。三次运算可以得到更接近高分辨率图像的放大效果,但也导致了运算量的急剧增加。
假设源图像A大小为mn,缩放K倍后的目标图像B的大小为MN,即K=M/m。A的每一个像素点是已知的,B是未知的,我们想要求出目标图像B中每一像素点(X,Y)的值,必须先找出像素(X,Y)在源图像A中对应的像素(x,y),再根据源图像A距离像素(x,y)最近的16个像素点作为计算目标图像B(X,Y)处像素值的参数,利用BiCubic基函数求出16个像素点的权重,图B像素(X,Y)的值就等于16个像素点的加权叠加。
在这里插入图片描述

根据比例关系x/X=m/M=1/K,我们可以得到B(X,Y)在A上的对应坐标为A(x,y)=A(X/K,Y/K)。如图所示P点就是目标图像B在(X,Y)处对应于源图像A中的位置,P的坐标位置会出现小数部分,所以我们假设 P的坐标为P(x+u,y+v),其中x,y分别表示整数部分,u,v分别表示小数部分(蓝点到a11方格中红点的距离)。那么我们就可以得到如图所示的最近16个像素的位置,在这里用a(i,j)(i,j=0,1,2,3)来表示,如上图。
BiCubic函数:在这里插入图片描述

BiCubic基函数是一维的,而像素是二维的,所以我们将像素点的行与列分开计算。
BiCubic函数中的参数x表示该像素点到P点的距离,例如a00距离P(x+u,y+v)的距离为(1+u,1+v),因此a00的横坐标权重i_0=W(1+u),纵坐标权重j_0=W(1+v)。
重点:距离是权重
a00对B(X,Y)的贡献值为:(a00像素值)* i_0* j_0。
因此,P点横坐标权重分别为W(1+u),W(u),W(1-u),W(2-u);纵坐标权重分别为W(1+v),W(v),W(1-v),W(2-v)。
写成矩阵形式,a也可以不取0.5,但是原作者的建议是0.5。在这里插入图片描述

S(x)是三次插值核函数,权重函数,可由如下公式近似:在这里插入图片描述

(2)python实现

'''@author: songcongcong'''

# 三次卷积的核函数
def S(x):
    if abs(x) <= 1: 
        y = 1- 2*np.power(x,2) + abs(np.power(x,3))
    elif abs(x)>1 and abs(x)<2:
        y = 4 - 8*abs(x) + 5*np.power(x,2) - abs(np.power(x,3))
    else:
        y = 0
    return y

# 三次卷积插值
def bicubic_interpolation(src, dst_shape):
    # 获取原图维度
    src_height, src_width = src.shape[0], src.shape[1]
    # 计算新图维度 注意channel数要想同
    dst_height, dst_width, channels = dst_shape[0], dst_shape[1], dst_shape[2]
    
    dst = np.zeros(shape = (dst_height, dst_width, channels), dtype=np.uint8)
    for dst_x in range(dst_height):
        for dst_y in range(dst_width):
            # 寻找源图像对应坐标
            src_x = (dst_x+0.5) * (src_width/dst_width) - 0.5
            src_y = (dst_y+0.5) * (src_width/dst_width) - 0.5
            i, j = int(src_x), int(src_y)
            u, v = src_x - i, src_y - j
            
            # 边界条件
            x1 = min(max(0, i-1), src_height-4)
            x2 = x1 + 4
            y1 = min(max(0, j-1), src_width-4)
            y2 = y1 + 4
            
            # 计算双三次插值
            A = np.array([S(u+1), S(u), S(u-1), S(u-2)])
            C = np.array([S(v+1), S(v), S(v-1), S(v-2)])
            B = src[x1:x2, y1:y2]
            f0 = [A @ B[..., i] @ C.T for i in range(channels)]
            f1 = np.stack(f0)
            f = np.clip(f1, 0, 255)  # 处理一下越界的数据

            # 插值
            dst[dst_x, dst_y,:] = f.astype(np.uint8)
    return dst

在这里插入图片描述
如果觉得不错请点赞鼓励,蟹蟹~

;