圖像縮放處理 - 雙線性插值算法,附加分析opencv(resize)源碼性能優化,含python代碼

圖像縮放處理-雙線性插值算法

一、概念

雙線性插值,又稱爲雙線性內插。在數學上,雙線性插值是對線性插值在二維直角網格上的擴展,用於對雙變量函數(例如 x 和 y)進行插值。其核心思想是在兩個方向分別進行一次線性插值。

舉例如下:
附:維基百科–雙線性插值:
在這裏插入圖片描述
假如我們想得到未知函數 f 在點 P = (x,y) 的值,
假設我們已知函數 f 在 Q11 = (x1,y1),Q12 = (x1,y2),Q21 = (x2,y1),Q22 = (x2,y2) 四個點的值。
首先在 X 方向進行線性插值,得到:
在這裏插入圖片描述
然後在 y 方向進行線性插值,得到:
在這裏插入圖片描述以上便是採用雙線性插值,得到的 P (x,y) 的值 f(x,y)。

注意此處如果先在 y 方向插值、再在 x 方向插值,其結果與按照上述順序雙線性插值的結果是一樣的。

二、圖像處理中的 雙線性插值函數:

  插值法是一種根據原圖(source)圖片信息構造目標圖像(destination)的方法。而其中的雙線性插值法是一種目前使用較多的插值方法,他在精度和計算量之間有較好的權衡,所以使用較爲廣泛。

1.最近鄰插值法:

給定一個圖片像素的矩陣,將1個3∗33∗3尺寸的矩陣使用最近鄰插值法到4∗4的矩陣
[123456789](src) \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}\tag{src}

[](dst) \begin{bmatrix} ?& ?& ?& ? \\ ?& ?& ?& ? \\ ?& ?& ?& ? \\ ?& ?& ?& ? \end{bmatrix}\tag{dst}

原始圖片像素矩陣(src)和縮放後圖片像素矩陣(dst)的對應對應關係公式如下:\color{red}(博客第三部分會對此公式進行優化)

src_x = dst_x * (src_width / dst_width)
src_y = dst_y * (src_heght / dst_height)

dst[0][0] 對應的src位置如下:
src_x = 0 * (3 / 4) = 0
src_y = 0 * (3 / 4) = 0
所以dst[0][0] = src[src_x][src_y] = src[0][0] = 1

dst[0][1] 對應的src位置如下:
src_x = 0 * (3 / 4) = 0
src_y = 1 * (3 / 4) = 0.75
所以dst[0][0] = src[src_x][src_y] = src[0][0.75]
由於圖片像素最小單位是1,所以四捨五入dst[0][1] = src[0][1] = 2

以此類推,最終結果如下:
[1233456678997899](dst) \begin{bmatrix} 1& 2& 3& 3 \\ 4& 5& 6& 6 \\ 7& 8& 9& 9 \\ 7& 8& 9& 9 \end{bmatrix}\tag{dst}

  這是一種最基本、最簡單的圖像縮放算法,效果也是最不好的,放大後的圖像有很嚴重的馬賽克,縮小後的圖像有很嚴重的失真;
效果不好的根源就是其簡單的最近鄰插值方法引入了嚴重的圖像失真,比如,當由目標圖的座標反推得到的源圖的的座標是一個\color{red}浮點數的時候,採用了\color{red}四捨五入的方法,直接採用了和這個浮點數最接近的象素的值,這種方法是很不科學的,當推得座標值爲 0.75的時候,不應該就簡單的取爲1,既然是0.75,比1要小0.25 ,比0要大0.75 ,那麼目標象素值其實應該根據這個源圖中虛擬的點四周的四個真實的點來按照一定的規律計算出來的,這樣才能達到更好的縮放效果。

2. 雙線性插值法

  雙線型內插值算法就是一種比較好的圖像縮放算法,它充分的利用了源圖中虛擬點四周的四個真實存在像素點的值(灰度值或者RGB值)來共同決定目標圖中的一個像素點的值(灰度值或者RGB值),因此縮放效果比簡單的最鄰近插值要好很多,縮放後圖像質量高,不會出現值(灰度值或者RGB值)不連續的情況。

雙線性內插值算法\color{red}核心描述如下:

  對於一個目的像素點dst[x][y],通過反向變換得到的源圖像中 浮點座標爲 src[i + u][j + v] (其中i、j均爲浮點座標的整數部分,u、v爲浮點座標的小數部分,是取值[0,1)區間的浮點數),則這個像素點的值 f(i+u,j+v) 可由原圖像中座標爲 (i,j)、(i+1,j)、(i,j+1)、(i+1,j+1)所對應的RGB\color{red}最相鄰四個像素點的值(灰度值或者RGB值)決定,即:
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)
其中f(i,j)表示源圖像(i,j)處的的值(灰度值或者RGB值),以此類推。

舉例便於大夥理解:

  假如目標圖的象素座標爲 (1,1),反推得到的對應於源圖的座標是(0.75 , 0.75), 這其實只是一個概念上的虛擬象素,實際在源圖中並不存在這樣一個象素,那麼目標圖的象素(1,1)的取值不能夠由這個虛擬象素來決定,而只能由源圖的這四個象素共同決定:(0,0)(0,1)(1,0)(1,1),而由於(0.75,0.75)離(1,1)要更近一些,那麼(1,1)所起的決定作用更大一些,這從公式1中的係數uv=0.75×0.75就可以體現出來,而(0.75,0.75)離(0,0)最遠,所以(0,0)所起的決定作用就要小一些,公式中係數爲(1-u)(1-v)=0.25×0.25也體現出了這一特點。
\color{red}即將最近的四個點採用加權平均,離得越近的點的權值越大。

算法完整步驟詳述:

  假設原始圖像大小爲size=m×n,其中m與n分別是原始圖像的行數與列數。
若圖像的縮放因子是t(t>0),則目標圖像的大小size=t×m×t×n。對於目標圖像的某個像素點P(x,y)通過P*1/t可得到對應的原始圖像座標P’( x1,y1),其中x1=x/t,y1=y/t,由於x1,y1都不是整數所以並不存在這樣的點,這樣可以找出與它相鄰的四個像素點的值(灰度值或者RGB值)、f2、f3、f4,使用雙線性插值算法就可以得到這個像素點P’(x1,y1)的值(灰度值或者RGB值),也就是像素點P(x,y)的值(灰度值或者RGB值)。

一個完整的雙線性插值算法可描述如下:

  1. 通過原始圖像和比例因子得到新圖像的大小,並用零矩陣初始化新圖像。
  2. 由新圖像的某個像素點(x,y)映射到原始圖像(x’,y’)處。
  3. 對x’,y’取整得到(xx,yy)並得到(xx,yy)、(xx+1,yy)、(xx,yy+1)和(xx+1,yy+1)的值。
  4. 利用雙線性插值得到像素點(x,y)的值並寫回新圖像。

重複步驟2,3 直到新圖像的所有像素點寫完。

三、分析opencv(resize)源碼進行性能優化:

  如果採用以上的公式進行編程實現,會發現計算出來的結果會和使用openCV對應的resize()函數得到的結果完全不一樣,而且從直觀上觀察,圖片縮放越小,差異越明顯,上面的算法會使縮放之後的圖片失真。

那麼opencv到底是做了哪些優化呢?

主要原因: openCV 的 resize 函數對與這個公式進行了改進

src_x = dst_x * (src_width / dst_width)
src_y = dst_y * (src_heght / dst_height)

改爲如下:

src_x = (j + 0.5) * float(org_w / dst_w) - 0.5
src_y = (i + 0.5) * float(org_h / dst_h) - 0.5

  通過在公式中進行 +/0.5\color{red}+/-0.5 的處理,從來將放縮前後兩個圖像的幾何中心重合,避免圖像邊緣上部分像素實際上並沒有參與計算的問題。
爲什麼通過 +/-0.5 可以實現將前後兩個圖像的集合中心重合,本人暫時還沒有想明白
本人也是通過手撕opencv的resize.cpp的源碼得出這樣的結論。
在這裏插入圖片描述
有興趣的朋友可以一起來研究一下opencv的源碼,還是比較有意義的。
github:https://github.com/opencv/opencv

參考資料:

維基百科:https://zh.wikipedia.org/zh-cn/%E5%8F%8C%E7%BA%BF%E6%80%A7%E6%8F%92%E5%80%BC
博客:

  1. 雙線性插值算法:
    https://blog.csdn.net/xiaqunfeng123/article/details/17362881
  2. opencv處理放縮前後圖片中心重合問題:
    https://www.cnblogs.com/funny-world/p/3162003.html

四、python代碼實現:

#!/usr/bin/env python
# encoding: utf-8
'''
@Author  : pentiumCM
@Email   : [email protected]
@Software: PyCharm
@File    : bilinear_interpolation.py
@Time    : 2020/3/7 11:01
@desc	 : 雙線性插值,來調整圖像的尺寸
'''

import numpy as np
import cv2

import math


def bilinear(src_img, dst_shape):
    """
    雙線性插值法,來調整圖片尺寸

    :param org_img: 原始圖片
    :param dst_shape: 調整後的目標圖片的尺寸
    :return:    返回調整尺寸後的圖片矩陣信息
    """
    dst_img = np.zeros((dst_shape[0], dst_shape[1], 3), np.uint8)
    dst_h, dst_w = dst_shape
    src_h = src_img.shape[0]
    src_w = src_img.shape[1]
    # i:縱座標y,j:橫座標x
    # 縮放因子,dw,dh
    scale_w = src_w / dst_w
    scale_h = src_h / dst_h

    for i in range(dst_h):
        for j in range(dst_w):
            src_x = float((j + 0.5) * scale_w - 0.5)
            src_y = float((i + 0.5) * scale_h - 0.5)
			
			# 向下取整,代表靠近源點的左上角的那一點的行列號	
            src_x_int = math.floor(src_x)
            src_y_int = math.floor(src_y)
            
            # 取出小數部分,用於構造權值
            src_x_float = src_x - src_x_int
            src_y_float = src_y - src_y_int

            if src_x_int + 1 == src_w or src_y_int + 1 == src_h:
                dst_img[i, j, :] = src_img[src_y_int, src_x_int, :]
                continue
            dst_img[i, j, :] = (1. - src_y_float) * (1. - src_x_float) * src_img[src_y_int, src_x_int, :] + \
                               (1. - src_y_float) * src_x_float * src_img[src_y_int, src_x_int + 1, :] + \
                               src_y_float * (1. - src_x_float) * src_img[src_y_int + 1, src_x_int, :] + \
                               src_y_float * src_x_float * src_img[src_y_int + 1, src_x_int + 1, :]
    return dst_img


if __name__ == '__main__':
    img_path = 'F:/experiment/image_identification/License_plate_recognition/Img/1.jpg'

    src = cv2.imread(img_path, cv2.IMREAD_COLOR)
    # 高337 - 寬500
    src_shape = (src.shape[0], src.shape[1])
    # 定義圖片縮放後的尺寸
    dst_shape = (100, 400)

    # 圖像放縮均採用雙線性插值法
    # opencv的放縮圖像函數
    resize_image = cv2.resize(src, (400, 100), interpolation=cv2.INTER_LINEAR)

    # 自定義的圖像放縮函數
    dst_img = bilinear(src, dst_shape)

    cv2.imwrite('F:/experiment/image_identification/License_plate_recognition/Img/1_new_resize.jpg', resize_image)
    cv2.imwrite('F:/experiment/image_identification/License_plate_recognition/Img/1_new.jpg', dst_img)

附上: 直觀效果圖

如下圖所示:

我們要對這一張圖片進行縮放:
在這裏插入圖片描述

  1. 這是使用雙線性插值原始的公式生成的效果圖:
    在這裏插入圖片描述

  2. 這是 修改過公式之後 使用0.5座標平移使縮放前後中心重合的效果圖:
    在這裏插入圖片描述

  3. 這是使用 opencv 的 resize 函數進行縮放的效果圖:
    在這裏插入圖片描述

可以看到第一種情況中:圖片上的車牌已經出現失真的問題,而優化之後的第二種情況同opencv的resize方式,沒有出現失真情況。

精度遺憾:

  本人比對了opencv中resize方式的圖片矩陣,和自定義算法處理縮放得到的圖片矩陣,還是存在矩陣中極少部分中的數據有細微精度偏差。比如這個實驗中,圖片的RGB值會誤差1,這應該是opencv源碼中C++部分對於數據進行的精度優化,如果有了解這一誤差緣由的大佬,希望得到您的指點。
截選了部分數據供與參考

  1. opencv(resize)的圖片矩陣數據:
    在這裏插入圖片描述
  2. 自定義的雙線性插值算法:
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章