【圖像縮放】雙立方(三次)卷積插值

前言

圖像處理中有三種經常使用的插值算法:git

  • 最鄰近插值github

  • 雙線性插值算法

  • 雙立方(三次卷積)插值canvas

其中效果最好的是雙立方(三次卷積)插值,本文介紹它的原理以及使用數組

若是想先看效果和源碼,能夠拉到最底部函數

本文的契機是某次基於canvas作圖像處理時,發現canvas自帶的縮放功能不盡人意,因而重溫了下幾種圖像插值算法,並整理出來。ui

爲什麼要進行雙立方插值

  • 對圖像進行插值的目的是爲了獲取縮小或放大後的圖片spa

  • 經常使用的插值算法中,雙立方插值效果最好.net

  • 本文中介紹雙立方插值的一些數學理論以及實現3d

雙立方三次卷積只是這個插值算法的兩種不一樣叫法而已,能夠自行推導,會發現最終能夠將求值轉化爲卷積公式

另外,像Photoshop等圖像處理軟件中也有這三種算法的實現

數學理論

雙立方插值計算涉及到16個像素點,以下圖

簡單分析以下:

  • 其中P00表明目標插值圖中的某像素點(x, y)在原圖中最接近的映射點

    • 譬如映射到原圖中的座標爲(1.1, 1.1),那麼P00就是(1, 1)
  • 而最終插值後的圖像中的(x, y)處的值即爲以上16個像素點的權重卷積之和

下圖進一步分析

以下是對圖的一些簡單分析

  • 譬如計算插值圖中(distI, distJ)處像素的值

  • 首先計算它映射到原圖中的座標(i + v, j + u)

  • 也就是說,卷積計算時,p00點對應(i, j)座標

  • 最終,插值後的圖(distI, distJ)座標點對應的值是原圖中(i, j)鄰近16個像素點的權重卷積之和

    • i, j的範圍是[i - 1, i + 2][j - 1, j + 2]

卷積公式

  • 設採樣公式爲S(x)

  • 原圖中每個(i, j)座標點的值得表達式爲f(i, j)

  • 插值後對應座標的值爲F(i + v, j + u)(這個值會做爲(distI, distJ)座標點的值)

那麼公式爲:

等價於(可自行推導)

提示

必定要區分本文中v, urow, col的對應關係,v表明行數誤差,u表明列數誤差(若是混淆了,會形成最終的圖像誤差很大)

如何理解卷積?

這是大學數學內容,推薦看看這個答案如何通俗易懂的解釋卷積-知乎

採樣公式

在卷積公式中有一個S(x),它就是關鍵的卷積插值公式

不一樣的公式,插值效果會有所差別(會致使加權值不同)

本文中採用WIKI-Bicubic interpolation中給出的插值公式:

公式中的特色是:

  • S(0) = 1

  • S(n) = 0(當n爲整數時)

  • 當x超出範圍時,S(x)爲0

  • a取不一樣值時能夠用來逼近不一樣的樣條函數(經常使用值-0.5, -0.75

當a取值爲-1

公式以下:

此時,逼近的函數是y = sin(x*PI)/(x*PI),如圖

當a取值爲-0.5

公式以下:

此時對應三次Hermite樣條

不一樣a的簡單對比

推導

可參考:

關於網上的一些推導公式奇怪實現

在網上查找了很多相關資料,發現有很多文章中都用到了如下這個奇怪的公式(譬如百度搜索雙立方插值

通常這些文章中都聲稱這個公式是用來近似y = sin(x*PI)/(x)

但事實上,進過驗證,它與y = sin(x*PI)/(x)相差甚遠(如上圖中是將sin函數縮放到合理係數後比對)

因爲相似的文章較多,年代都比較久遠,無從得知最初的來源

多是某文中漏掉了分母的PI,亦或是這個公式只是某文本身實現的一個採樣公式,與sin無關,而後被誤傳了。

這裏都無從考據,僅此記錄,避免疑惑。

另外一種基於係數的實現

能夠參考:圖像處理(一)bicubic解釋推導

像這類的實現就是直接計算最原始的係數,而後經過16個像素點計算不一樣係數值,最終計算出目標像素

本質是同樣的,只不過是沒有基於最終的卷積方程計算而已(也就是說在原始理論階段沒有推成插值公式,而是直接解出係數並計算)。

代碼實如今github項目中可看到,參考最後的開源項目

代碼實現

如下是JavaScript代碼實現的插值核心方程

/** * 採樣公式的常數A取值,調整銳化與模糊 * -0.5 三次Hermite樣條 * -0.75 經常使用值之一 * -1 逼近y = sin(x*PI)/(x*PI) * -2 經常使用值之一 */
const A = -0.5;

function interpolationCalculate(x) {
    const absX = x > 0 ? x : -x;
    const x2 = x * x;
    const x3 = absX * x2;

    if (absX <= 1) {
        return 1 - (A + 3) * x2 + (A + 2) * x3;
    } else if (absX <= 2) {
        return -4 * A + 8 * A * absX - 5 * A * x2 + A * x3;
    }
    return 0;
}複製代碼

以上是卷積方程的核心實現。下面則是一套完整的實現

/** * 採樣公式的常數A取值,調整銳化與模糊 * -0.5 三次Hermite樣條 * -0.75 經常使用值之一 * -1 逼近y = sin(x*PI)/(x*PI) * -2 經常使用值之一 */
const A = -1;

function interpolationCalculate(x) {
    const absX = x >= 0 ? x : -x;
    const x2 = x * x;
    const x3 = absX * x2;

    if (absX <= 1) {
        return 1 - (A + 3) * x2 + (A + 2) * x3;
    } else if (absX <= 2) {
        return -4 * A + 8 * A * absX - 5 * A * x2 + A * x3;
    }

    return 0;
}

function getPixelValue(pixelValue) {
    let newPixelValue = pixelValue;

    newPixelValue = Math.min(255, newPixelValue);
    newPixelValue = Math.max(0, newPixelValue);

    return newPixelValue;
}

/** * 獲取某行某列的像素對於的rgba值 * @param {Object} data 圖像數據 * @param {Number} srcWidth 寬度 * @param {Number} srcHeight 高度 * @param {Number} row 目標像素的行 * @param {Number} col 目標像素的列 */
function getRGBAValue(data, srcWidth, srcHeight, row, col) {
    let newRow = row;
    let newCol = col;

    if (newRow >= srcHeight) {
        newRow = srcHeight - 1;
    } else if (newRow < 0) {
        newRow = 0;
    }

    if (newCol >= srcWidth) {
        newCol = srcWidth - 1;
    } else if (newCol < 0) {
        newCol = 0;
    }

    let newIndex = (newRow * srcWidth) + newCol;

    newIndex *= 4;

    return [
        data[newIndex + 0],
        data[newIndex + 1],
        data[newIndex + 2],
        data[newIndex + 3],
    ];
}

function scale(data, width, height, newData, newWidth, newHeight) {
    const dstData = newData;

    // 計算壓縮後的縮放比
    const scaleW = newWidth / width;
    const scaleH = newHeight / height;

    const filter = (dstCol, dstRow) => {
        // 源圖像中的座標(多是一個浮點)
        const srcCol = Math.min(width - 1, dstCol / scaleW);
        const srcRow = Math.min(height - 1, dstRow / scaleH);
        const intCol = Math.floor(srcCol);
        const intRow = Math.floor(srcRow);
        // 計算u和v
        const u = srcCol - intCol;
        const v = srcRow - intRow;

        // 真實的index,由於數組是一維的
        let dstI = (dstRow * newWidth) + dstCol;

        dstI *= 4;

        // 存儲灰度值的權重卷積和
        const rgbaData = [0, 0, 0, 0];
        // 根據數學推導,16個點的f1*f2加起來是趨近於1的(可能會有浮點偏差)
        // 所以就再也不單獨先加權值,再除了
        // 16個鄰近點
        for (let m = -1; m <= 2; m += 1) {
            for (let n = -1; n <= 2; n += 1) {
                const rgba = getRGBAValue(
                    data,
                    width,
                    height,
                    intRow + m,
                    intCol + n,
                );
                // 必定要正確區分 m,n和u,v對應的關係,不然會形成圖像嚴重誤差(譬如出現噪點等)
                // F(row + m, col + n)S(m - v)S(n - u)
                const f1 = interpolationCalculate(m - v);
                const f2 = interpolationCalculate(n - u);
                const weight = f1 * f2;

                rgbaData[0] += rgba[0] * weight;
                rgbaData[1] += rgba[1] * weight;
                rgbaData[2] += rgba[2] * weight;
                rgbaData[3] += rgba[3] * weight;
            }
        }

        dstData[dstI + 0] = getPixelValue(rgbaData[0]);
        dstData[dstI + 1] = getPixelValue(rgbaData[1]);
        dstData[dstI + 2] = getPixelValue(rgbaData[2]);
        dstData[dstI + 3] = getPixelValue(rgbaData[3]);
    };

    // 區塊
    for (let col = 0; col < newWidth; col += 1) {
        for (let row = 0; row < newHeight; row += 1) {
            filter(col, row);
        }
    }
}

export default function bicubicInterpolation(imgData, newImgData) {
    scale(imgData.data,
        imgData.width,
        imgData.height,
        newImgData.data,
        newImgData.width,
        newImgData.height);

    return newImgData;
}複製代碼

運行效果

分別用三種算法對一個圖進行放大,能夠明顯的看出雙立方插值效果最好

最臨近插值

雙線性插值

雙立方(三次卷積)插值

開源項目

這個項目裏用JS實現了幾種插值算法,包括(最鄰近值,雙線性,三次卷積-包括兩種不一樣實現等)

github.com/dailc/image…

附錄

參考資料

相關文章
相關標籤/搜索