天天看點

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

說明

【跟月影學可視化】學習筆記。

如何了解像素化?

像素化

所謂像素化,就是把一個圖像看成是由一組像素點組合而成的。每個像素點負責描述圖像上的一個點,并且帶有這個點的基本繪圖資訊。

像素點是怎麼存儲的?

Canvas2D 以 4 個通道來存放每個像素點的顔色資訊,每個通道是 8 個比特位,也就是 0~255 的十進制數值,4 個通道對應 RGBA 顔色的四個值。

應用一:實作灰階化圖檔

什麼是灰階化?

灰階化,在RGB模型中,如果R=G=B時,則彩色表示一種灰階顔色,其中R=G=B的值叫灰階值,是以,灰階圖像每個像素隻需一個位元組存放灰階值(又稱強度值、亮度值),灰階範圍為0-255。

灰階化圖檔:簡單來說就是将一張彩色圖檔變為灰白色圖檔。

灰階化的原理

實作思路:先将該圖檔的每個像素點的 R、G、B 通道的值進行權重平均,然後将這個值作為每個像素點新的 R、G、B 通道值,具體公式如下:

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

其中 R、G、B 是原圖檔中的 R、G、B 通道的色值,V 是權重平均色值,a、b、c 是權重系數,滿足 (a + b + c) = 1。

用權重平均的計算公式來替換圖檔的 RGBA 的值。這本質上其實是利用線性方程組改變了圖檔中每一個像素的 RGB 通道的原色值,将每個通道的色值映射為一個新色值。

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

灰階化圖檔的過程

  1. 加載圖檔
  2. 繪制圖檔到canvas
  3. 擷取 imageData 資訊
  4. 循環處理每個像素的顔色資訊
  5. 最後寫入canvas

我們先去找一張圖檔,等下實作灰階化圖檔例子需要:​​https://unsplash.com/photos/QRBuN0wNm-8​​

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

我們先了解一下圖檔的像素資訊,圖檔的全部像素資訊會以類型數組(​

​Uint8ClampedArray​

​)的形式儲存在 ImageData 對象的 data 屬性裡,而類型數組的每 4 個元素組成一個像素的資訊,這四個元素依次表示該像素的 RGBA 四通道的值,是以它的資料結構如下:

data[0] // 第1行第1列的紅色通道值
data[1] // 第1行第1列的綠色通道值
data[2] // 第1行第1列的藍色通道值
data[3] // 第1行第1列的Alpha通道值
data[4] // 第1行第2列的紅色通道值
data[5] // 第1行第2列的綠色通道值
...      

代碼實作:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>灰階化圖檔</title>
    </head>
    <body>
        <canvas id="paper" width="0" height="0"></canvas>
        <script type="module">
            import {
                loadImage,
                getImageData,
                traverse,
            } from "./common/lib/util.js";

            const canvas = document.getElementById("paper");
            const context = canvas.getContext("2d");

            (async function () {
                // 異步加載圖檔
                const img = await loadImage(
                    "https://images.unsplash.com/photo-1666552982368-dd0e2bb96993?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80"
                );
                // 擷取圖檔的 imageData 資料對象
                const imageData = getImageData(img);
                console.log("imageData---->", imageData);
                // 周遊 imageData 資料對象:traverse 函數會自動周遊圖檔的每個像素點,把獲得的像素資訊傳給參數中的回調函數處理
                traverse(imageData, ({ r, g, b, a }) => {
                    // 對每個像素進行灰階化處理
                    const v = 0.2126 * r + 0.7152 * g + 0.0722 * b;
                    return [v, v, v, a];
                });
                // 更新canvas内容
                canvas.width = imageData.width;
                canvas.height = imageData.height;
                // 将資料從已有的 ImageData 對象繪制到位圖
                context.putImageData(imageData, 0, 0);
            })();
        </script>
    </body>
</html>      

抽離的邏輯:​

​/common/lib/util.js​

// 異步加載圖檔
export function loadImage(src) {
    const img = new Image();
    img.crossOrigin = "anonymous";
    return new Promise((resolve) => {
        img.onload = () => {
            resolve(img);
        };
        img.src = src;
    });
}

const imageDataContext = new WeakMap();
// 獲得圖檔的 imageData 資料
export function getImageData(img, rect = [0, 0, img.width, img.height]) {
    let context;
    if (imageDataContext.has(img)) {
        context = imageDataContext.get(img)
    } else {
        // OffscreenCanvas 提供了一個可以脫離螢幕渲染的 canvas 對象。它在視窗環境和web worker環境均有效。
        const canvas = new OffscreenCanvas(img.width, img.height);
        context = canvas.getContext("2d");
        context.drawImage(img, 0, 0);
        imageDataContext.set(img, context);
    }
    console.log("imageDataContext---->", imageDataContext);
    // CanvasRenderingContext2D.getImageData() 傳回一個ImageData對象,用來描述 canvas 區域隐含的像素資料
    return context.getImageData(...rect);
}

// 循環周遊 imageData 資料
export function traverse(imageData, pass) {
    const { width, height, data } = imageData;
    // width * height * 4:圖檔一共是width * height 個像素點,每個像素點有 4 個通道
    for (let i = 0; i < width * height * 4; i += 4) {
        // 除以 255 為了做歸一化,接受的是0~1的值,友善矩陣運算
        const [r, g, b, a] = pass({
            r: data[i] / 255,
            g: data[i + 1] / 255,
            b: data[i + 2] / 255,
            a: data[i + 3] / 255,
            index: i,
            width,
            height,
            x: ((i / 4) % width) / width,
            y: Math.floor(i / 4 / width) / height,
        });
        data.set(
            [r, g, b, a].map((v) => Math.round(v * 255)),
            i
        );
    }
    return imageData;
}      
【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

應用二:使用像素矩陣通用地改變像素顔色

建立一個 ​

​4*5​

​ 顔色矩陣,讓它的第一行決定紅色通道,第二行決定綠色通道,第三行決定藍色通道,第四行決定 Alpha 通道。

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

如果要改變一個像素的顔色效果,隻需要将該矩陣與像素的顔色向量相乘即可。通常把傳回顔色矩陣的函數,一般稱為顔色濾鏡函數。

灰階化 grayscale 函數

人的視覺對 R、G、B 三色通道的敏感度是不一樣的,對綠色敏感度高,是以權重值高,對藍色敏感度低,是以權重值低。

// 參數 p,它是一個 0~1 的值,表示灰階化的程度,1 是完全灰階化,0 是完全不灰階(原始色彩)。
function grayscale(p = 1) {
  const r = 0.2126 * p;
  const g = 0.7152 * p;
  const b = 0.0722 * p;
  
  return [
     r + 1 - p, g, b, 0, 0,
     r, g + 1 - p, b, 0, 0,
     r, g, b + 1 - p, 0, 0,
     0, 0, 0, 1, 0,
  ];
}      

過濾或增強某個顔色通道 channel 函數

function channel({r = 1, g = 1, b = 1}) {
  return[
    r, 0,0, 0, 0,
    0, g, 0, 0, 0,
    0, 0, b, 0, 0,
    0, 0, 0, 1, 0,
  ];
}      

亮度(Brightness)函數

// 改變亮度,p = 0 全暗,p > 0 且 p < 1 調暗,p = 1 原色, p > 1 調亮
function brightness(p) {
  return [
    p, 0, 0, 0, 0,
    0, p, 0, 0, 0,
    0, 0, p, 0, 0,
    0, 0, 0, 1, 0,
  ];
}      

飽和度(Saturate)函數

// 飽和度,與grayscale正好相反 p = 0 完全灰階化,p = 1 原色,p > 1 增強飽和度
function saturate(p) {
  const r = 0.212 * (1 - p);
  const g = 0.714 * (1 - p);
  const b = 0.074 * (1 - p);
  return [
    r + p, g, b, 0, 0,
    r, g + p, b, 0, 0,
    r, g, b + p, 0, 0,
    0, 0, 0, 1, 0,
  ];
}      

對比度(Constrast)函數

// 對比度, p = 1 原色, p < 1 減弱對比度,p > 1 增強對比度
function contrast(p) {
  const d = 0.5 * (1 - p);
  return [
    p, 0, 0, 0, d,
    0, p, 0, 0, d,
    0, 0, p, 0, d,
    0, 0, 0, 1, 0,
  ];
}      

透明度(Opacity)函數

// 透明度,p = 0 全透明,p = 1 原色
function opacity(p) {
  return [
    1, 0, 0, 0, 0,
    0, 1, 0, 0, 0,
    0, 0, 1, 0, 0,
    0, 0, 0, p, 0,
  ];
}      

反色(Invert)函數

// 反色, p = 0 原色, p = 1 完全反色
function invert(p) {
  const d = 1 - 2 * p;
  return [
    d, 0, 0, 0, p,
    0, d, 0, 0, p,
    0, 0, d, 0, p,
    0, 0, 0, 1, 0,
  ];
}      

旋轉色相(HueRotate)函數

// 色相旋轉,将色調沿極坐标轉過deg角度
export function hueRotate(deg) {
  const rotation = deg / 180 * Math.PI;
  const cos = Math.cos(rotation),
    sin = Math.sin(rotation),
    lumR = 0.213,
    lumG = 0.715,
    lumB = 0.072;
  return [
    lumR + cos * (1 - lumR) + sin * (-lumR), lumG + cos * (-lumG) + sin * (-lumG), lumB + cos * (-lumB) + sin * (1 - lumB), 0, 0,
    lumR + cos * (-lumR) + sin * (0.143), lumG + cos * (1 - lumG) + sin * (0.140), lumB + cos * (-lumB) + sin * (-0.283), 0, 0,
    lumR + cos * (-lumR) + sin * (-(1 - lumR)), lumG + cos * (-lumG) + sin * (lumG), lumB + cos * (1 - lumB) + sin * (lumB), 0, 0,
    0, 0, 0, 1, 0,
  ];
}      

實戰:讓一張圖檔變得有“陽光感”

我們使用疊加 channel 函數中的紅色通道、brightness 函數和 saturate 函數來實作。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>讓一張圖檔變得有陽光感</title>
    </head>
    <body>
        <img src="https://images.unsplash.com/photo-1666552982368-dd0e2bb96993?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80" alt="">
        <canvas id="paper" width="0" height="0"></canvas>
        <script type="module">
            import {
                loadImage,
                getImageData,
                traverse,
            } from "./common/lib/util.js";
            import { transformColor, channel, brightness, saturate } from "./common/lib/color-matrix.js";

            const canvas = document.getElementById("paper");
            const context = canvas.getContext("2d");

            (async function () {
                // 異步加載圖檔
                const img = await loadImage(
                    "https://images.unsplash.com/photo-1666552982368-dd0e2bb96993?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80"
                );
                // 擷取圖檔的 imageData 資料對象
                const imageData = getImageData(img);
                console.log("imageData---->", imageData);
                // 周遊 imageData 資料對象:traverse 函數會自動周遊圖檔的每個像素點,把獲得的像素資訊傳給參數中的回調函數處理
                traverse(imageData, ({ r, g, b, a }) => {
                    // 将 color 通過顔色矩陣映射成新的色值傳回
                    return transformColor([r, g, b, a],
                        channel({r: 1.2}), // 增強紅色通道
                        brightness(1.2), // 增強亮度
                        saturate(1.2), // 增強飽和度 
                    );
                });
                // 更新canvas内容
                canvas.width = imageData.width;
                canvas.height = imageData.height;
                // 将資料從已有的 ImageData 對象繪制到位圖
                context.putImageData(imageData, 0, 0);
            })();
        </script>
    </body>
</html>      
【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

應用三:使用高斯模糊對照片美顔

可視化裡為了突出呈現的内容,通常會使用顔色濾鏡來增強視覺呈現的細節,而用一種相對複雜的濾鏡來模糊背景,這個複雜濾鏡就叫做高斯模糊(​

​Gaussian Blur​

​)。高斯模糊是一個非常重要的平滑效果濾鏡(​

​Blur Filters​

​)。

高斯模糊的原理與顔色濾鏡不同,高斯模糊不是單純根據顔色矩陣計算目前像素點的顔色值,而是會按照高斯分布的權重,對目前像素點及其周圍像素點的顔色按照高斯分布的權重權重平均。具體的可以看文章的拓展部分。

二維高斯函數

計算式:

const a = 1 / (Math.sqrt(2 * Math.PI) * sigma);
const b = -1 / (2 * sigma ** 2);

const g = a * Math.exp(b * x ** 2);      
【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

高斯分布矩陣

這個矩陣的作用是按照高斯函數提供平滑過程中參與計算的像素點的權重平均權重。

function gaussianMatrix(radius, sigma = radius / 3) {
  const a = 1 / (Math.sqrt(2 * Math.PI) * sigma);
  const b = -1 / (2 * sigma ** 2);
  let sum = 0;
  const matrix = [];
  for(let x = -radius; x <= radius; x++) {
    const g = a * Math.exp(b * x ** 2);
    matrix.push(g);
    sum += g;
  }
  
  for(let i = 0, len = matrix.length; i < len; i++) {
    matrix[i] /= sum;
  }
  return {matrix, sum};
}      

高斯模糊函數

/**
  * 高斯模糊
  * @param  {Array} pixes  pix array
  * @param  {Number} width 圖檔的寬度
  * @param  {Number} height 圖檔的高度
  * @param  {Number} radius 取樣區域半徑, 正數, 可選, 預設為 3.0
  * @param  {Number} sigma 标準方差, 可選, 預設取值為 radius / 3
  * @return {Array}
  */
export function gaussianBlur(pixels, width, height, radius = 3, sigma = radius / 3) {
    const { matrix, sum } = gaussianMatrix(radius, sigma);
    // x 方向一維高斯運算
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            let r = 0,
                g = 0,
                b = 0;

            for (let j = -radius; j <= radius; j++) {
                const k = x + j;
                if (k >= 0 && k < width) {
                    const i = (y * width + k) * 4;
                    r += pixels[i] * matrix[j + radius];
                    g += pixels[i + 1] * matrix[j + radius];
                    b += pixels[i + 2] * matrix[j + radius];
                }
            }
            const i = (y * width + x) * 4;
            // 除以 sum 是為了消除處于邊緣的像素, 高斯運算不足的問題
            pixels[i] = r / sum;
            pixels[i + 1] = g / sum;
            pixels[i + 2] = b / sum;
        }
    }

    // y 方向一維高斯運算
    for (let x = 0; x < width; x++) {
        for (let y = 0; y < height; y++) {
            let r = 0,
                g = 0,
                b = 0;

            for (let j = -radius; j <= radius; j++) {
                const k = y + j;
                if (k >= 0 && k < height) {
                    const i = (k * width + x) * 4;
                    r += pixels[i] * matrix[j + radius];
                    g += pixels[i + 1] * matrix[j + radius];
                    b += pixels[i + 2] * matrix[j + radius];
                }
            }
            const i = (y * width + x) * 4;
            pixels[i] = r / sum;
            pixels[i + 1] = g / sum;
            pixels[i + 2] = b / sum;
        }
    }
    return pixels;
}      

例子:使用高斯模糊函數處理圖檔

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>高斯模糊</title>
    </head>
    <body>
        <img
            src="https://images.unsplash.com/photo-1666552982368-dd0e2bb96993?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80"
            alt=""
        />
        <canvas id="paper" width="0" height="0"></canvas>
        <script type="module">
            import {
                loadImage,
                getImageData,
                gaussianBlur,
            } from "./common/lib/util.js";

            const canvas = document.getElementById("paper");
            const context = canvas.getContext("2d");

            (async function () {
                // 異步加載圖檔
                const img = await loadImage("https://images.unsplash.com/photo-1666552982368-dd0e2bb96993?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80");
                // 擷取圖檔的 imageData 資料對象
                const imageData = getImageData(img);
                const { width, height, data } = imageData;
                // 對imageData應用高斯模糊:整體對圖檔所有像素應用高斯模糊函數。
                gaussianBlur(data, width, height, 30, 10);
                // 更新canvas内容
                canvas.width = imageData.width;
                canvas.height = imageData.height;
                // 将資料從已有的 ImageData 對象繪制到位圖
                context.putImageData(imageData, 0, 0);
            })();
        </script>
    </body>
</html>      

可以明顯的右邊是用了高斯模糊的

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

像素化與 CSS 濾鏡

如果隻是按照某些特定規則改變一個圖像上的所有像素,浏覽器提供了更簡便的方法:CSS濾鏡。

  • ​​CSS 濾鏡​​
  • ​​Canvas 濾鏡​​
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSS濾鏡</title>
</head>
<body>
    <img src="https://images.unsplash.com/photo-1666552982368-dd0e2bb96993?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80">
    <img 
        src="https://images.unsplash.com/photo-1666552982368-dd0e2bb96993?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80"
        style="filter:grayscale(100%)"
    >
    <img 
        src="https://images.unsplash.com/photo-1666552982368-dd0e2bb96993?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80"
        style="filter:blur(1.5px) grayscale(0.5) saturate(1.2) contrast(1.1) brightness(1.2)"
    >
</body>
</html>      
【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

相比起來CSS 濾鏡和 Canvas 濾鏡都很好用,但隻能實作比較固定的視覺效果。而用像素處理圖檔更靈活,因為它可以實作濾鏡功能,還可以實作更加豐富的效果,包括一些非常炫酷的視覺效果。

拓展:高斯模糊的算法

下面這些來自​​阮一峰的網絡日志:高斯模糊的算法​​

通常,圖像處理軟體會提供"模糊"(blur)濾鏡,使圖檔産生模糊的效果。

模糊的算法有很多種,其中有一種叫做高斯模糊(​

​Gaussian Blur​

​)。它将正态分布(又名高斯分布)用于圖像處理。

本質上,它是一種資料平滑技術(​

​data smoothing​

​),适用于多個場合,圖像處理恰好提供了一個直覺的應用執行個體。

一、高斯模糊的原理

所謂模糊,可以了解成每一個像素都取周邊像素的平均值。在圖形上,就相當于産生"模糊"效果,"中間點"失去細節。計算平均值時,取值範圍越大,"模糊效果"越強烈。

每個點都要取周邊像素的平均值,那麼應該如何配置設定權重呢?

二、正态分布的權重

正态分布顯然是一種可取的權重配置設定模式。

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

在圖形上,正态分布是一種鐘形曲線,越接近中心,取值越大,越遠離中心,取值越小。

計算平均值的時候,我們隻需要将"中心點"作為原點,其他點按照其在正态曲線上的位置,配置設定權重,就可以得到一個權重平均值。

三、高斯函數

上面的正态分布是一維的,圖像都是二維的,是以我們需要二維的正态分布。

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

正态分布的密度函數叫做高斯函數(Gaussian function)。它的一維形式是:

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

其中,μ是x的均值,σ是x的方差。因為計算平均值的時候,中心點就是原點,是以μ等于0。

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

根據一維高斯函數,可以推導得到二維高斯函數:

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

有了這個函數 ,就可以計算每個點的權重了。

四、權重矩陣

假定中心點的坐标是(0,0),那麼距離它最近的8個點的坐标如下:

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

為了計算權重矩陣,需要設定σ的值。假定σ=1.5,則模糊半徑為1的權重矩陣如下:

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

這9個點的權重總和等于0.4787147,如果隻計算這9個點的權重平均,還必須讓它們的權重之和等于1,是以上面9個值還要分别除以0.4787147,得到最終的權重矩陣。

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

五、計算高斯模糊

有了權重矩陣,就可以計算高斯模糊的值了。

假設現有9個像素點,灰階值(0-255)如下:

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

每個點乘以自己的權重值:

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

得到

【視覺基礎篇】12 # 如何使用濾鏡函數實作美顔效果?

将這9個值加起來,就是中心點的高斯模糊的值。

對所有點重複這個過程,就得到了高斯模糊後的圖像。如果原圖是彩色圖檔,可以對RGB三個通道分别做高斯模糊。

六、邊界點的處理

參考資料

  • ​​高斯模糊的算法​​

繼續閱讀