视觉基础篇12 # 如何使用滤镜函数实现美颜效果?

Posted 凯小默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了视觉基础篇12 # 如何使用滤镜函数实现美颜效果?相关的知识,希望对你有一定的参考价值。

说明

【跟月影学可视化】学习笔记。

如何理解像素化?

像素化

所谓像素化,就是把一个图像看成是由一组像素点组合而成的。每个像素点负责描述图像上的一个点,并且带有这个点的基本绘图信息。

像素点是怎么存储的?

Canvas2D 以 4 个通道来存放每个像素点的颜色信息,每个通道是 8 个比特位,也就是 0~255 的十进制数值,4 个通道对应 RGBA 颜色的四个值。

应用一:实现灰度化图片

什么是灰度化?

灰度化,在RGB模型中,如果R=G=B时,则彩色表示一种灰度颜色,其中R=G=B的值叫灰度值,因此,灰度图像每个像素只需一个字节存放灰度值(又称强度值、亮度值),灰度范围为0-255。

灰度化图片:简单来说就是将一张彩色图片变为灰白色图片。

灰度化的原理

实现思路:先将该图片的每个像素点的 R、G、B 通道的值进行加权平均,然后将这个值作为每个像素点新的 R、G、B 通道值,具体公式如下:

其中 R、G、B 是原图片中的 R、G、B 通道的色值,V 是加权平均色值,a、b、c 是加权系数,满足 (a + b + c) = 1。

用加权平均的计算公式来替换图片的 RGBA 的值。这本质上其实是利用线性方程组改变了图片中每一个像素的 RGB 通道的原色值,将每个通道的色值映射为一个新色值。

灰度化图片的过程

  1. 加载图片
  2. 绘制图片到canvas
  3. 获取 imageData 信息
  4. 循环处理每个像素的颜色信息
  5. 最后写入canvas

我们先去找一张图片,等下实现灰度化图片例子需要:https://unsplash.com/photos/QRBuN0wNm-8

我们先了解一下图片的像素信息,图片的全部像素信息会以类型数组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;

应用二:使用像素矩阵通用地改变像素颜色

创建一个 4*5 颜色矩阵,让它的第一行决定红色通道,第二行决定绿色通道,第三行决定蓝色通道,第四行决定 Alpha 通道。

如果要改变一个像素的颜色效果,只需要将该矩阵与像素的颜色向量相乘即可。通常把返回颜色矩阵的函数,一般称为颜色滤镜函数

灰度化 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), // 增强饱和度 
                    );
                );<

以上是关于视觉基础篇12 # 如何使用滤镜函数实现美颜效果?的主要内容,如果未能解决你的问题,请参考以下文章

Android平台Camera实时滤镜实现方法探讨--实时美颜滤镜

iOS实时美颜滤镜实现

如何在ffmpeg中使用滤镜技术

如何在ffmpeg中使用滤镜技术

图像滤镜艺术---微软自拍APP滤镜实现合集DEMO

如何快速实现直播美颜功能