说明
【跟月影学可视化】学习笔记。
如何理解像素化?
像素化
所谓像素化,就是把一个图像看成是由一组像素点组合而成的。每个像素点负责描述图像上的一个点,并且带有这个点的基本绘图信息。
像素点是怎么存储的?
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 通道的原色值,将每个通道的色值映射为一个新色值。
灰度化图片的过程
- 加载图片
- 绘制图片到canvas
- 获取 imageData 信息
- 循环处理每个像素的颜色信息
- 最后写入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), // 增强饱和度 ); }); // 更新canvas内容 canvas.width = imageData.width; canvas.height = imageData.height; // 将数据从已有的 ImageData 对象绘制到位图 context.putImageData(imageData, 0, 0); })(); </script> </body> </html>
应用三:使用高斯模糊对照片美颜
可视化里为了突出呈现的内容,通常会使用颜色滤镜来增强视觉呈现的细节,而用一种相对复杂的滤镜来模糊背景,这个复杂滤镜就叫做高斯模糊(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);
高斯分布矩阵
这个矩阵的作用是按照高斯函数提供平滑过程中参与计算的像素点的加权平均权重。
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>
可以明显的右边是用了高斯模糊的