webGL 可视化学习 —— 滤镜函数

83 阅读7分钟

1. 学习知识点

  1. 什么是像素化?
  2. 常见的滤镜函数有哪些,并且是如何实现的?
  3. 高斯模糊
  4. css 滤镜效果

\

2. 什么是像素化?

在可视化领域里,我们常常需要处理大规模的数据,比如,需要呈现数万甚至数十万条信息在空间中的分布情况。如果我们用几何绘制的方式将这些信息一一绘制出来,性能可能就会很差。这时,我们就可以将这些数据简化为像素点进行处理。这种处理图像的新思路就叫做像素化,而图片像素化是一个更加重要的手段。


2.1 如何理解像素化

首先,我们来理解两个基础的概念。第一个是像素化。所谓像素化,就是把一个图像看成是由一组像素点组合而成的。每个像素点负责描述图像上的一个点,并且带有这个点的基本绘图信息。那对于一张 800 像素宽、600 像素高的图片来说,整张图一共就有 48 万个像素点(48万 = 800 * 600)。

\

而每一个像素点的颜色,是按照4个通道来进行存储的。每个通道是8个比特位,也就是0~255的十进制数,4个通道对应RGBA颜色值。

其实也就是说,一张 800 * 600 的图片,它照片的体积大小是 800 * 600 * 4 /1024/1024 = 1.8 M , 但是我们现实中的图片一般都比这个小很多,这是为什么呢,大家可以思考下?


3. 常见的滤镜函数有哪些?

  1. 灰度化
  2. 亮度
  3. 饱和度
  4. 对比度
  5. 透明度
  6. 反色
  7. 色相旋转

在想使用滤镜函数时,我们需要先拿到图片的原始数据,因为滤镜函数原本就是对数据的操作。

3.1 获取图片的数据

function loadImage(src: string) {
  const img = new Image();
  img.crossOrigin = "anonymous";
  return new Promise<HTMLImageElement>((resolve) => {
    img.onload = () => {
      resolve(img);
    };
    img.src = src;
  });
}

const canvas = document.querySelector("#app") as HTMLCanvasElement;
const context = canvas.getContext("2d");

(async function () {
  const img = await loadImage("/src/assets/dlam.png");
  const { width, height } = img;
  
  canvas.width = width;
  canvas.height = height;
  context.drawImage(img, 0, 0);
  
  const imgData = context.getImageData(0, 0, width, height);
  
  // 操作数据
  
  context.putImageData(imgData, 0, 0);
  
})();

我们通过 drawImage 方法将图片写在canvas 当中,然后通过getImageData方法获取图片的原始数据,然后操作数据,使用 putImageData 方法,在把改变后的数据写在canvas中。

其主要流程如下:

3.2 灰度滤镜

\

灰度滤镜的转换规则

const v = 0.212 * r + 0.714 * g + 0.074 * b;

这里你可能会觉得奇怪,我们为什么用0.2126、0.7152和0.0722这三个权重,而不是都用算术平均值1/3呢?这是因为,人的视觉对 R、G、B 三色通道的敏感度是不一样的,对绿色敏感度高,所以加权值高,对蓝色敏感度低,所以加权值低。

处理成灰度值的代码:

 const imgData = context.getImageData(0, 0, width, height);
 const data = imgData.data;
 for (let i = 0; i < width * height * 4; i += 4) {
    const r = data[i],
      g = data[i + 1],
      b = data[i + 2],
      a = data[i + 3];

    const v = 0.212 * r + 0.714 * g + 0.074 * b;
    // const v = 0.33333 * r + 0.33333 * g + 0.3333333 * b;
    data[i] = v;
    data[i + 1] = v;
    data[i + 2] = v;
    data[i + 3] = a;
 }
 context.putImageData(imgData, 0, 0);

3. 3 优化代码

为了减少冗余我们对代码进行封装。

export function traverse(
  imageData: ImageData,
  pass: (params: {
    r: number;
    g: number;
    b: number;
    a: number;
    index: number;
    width: number;
    height: number;
    x: number;
    y: number;
  }) => [r: number, g: number, b: number, a: number]
) {
  const { width, height, data } = imageData;
  for (let i = 0; i < width * height * 4; i += 4) {
    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;
}

调用方式:

let imageData1 = getImagesData(img, rate)

// 灰度代码
traverse(imageData1, ({ r, g, b, a }) => {
    // 对每个像素进行灰度化处理;
    const v = 0.2126 * r + 0.7152 * g + 0.0722 * b;
    return [v, v, v, a];
  });

 context.putImageData(
    imageData1,
    1 * rate * canvas.width,
    0 * rate * canvas.height
  );

这样做的好处是,traverse 函数会自动遍历图片的每个像素点,把获得的像素信息传给参数中的回调函数处理。这样,我们就只关注 traverse 函数里面的处理过程就可以了。

3.4 使用矩阵表示滤镜函数

使用上面的方式,如果想实现不同的颜色变换,就必须使用不同的方程组,这会让我们使用起来非常麻烦,因此我们可以使用矩阵的方式实现这个问题,我们称这个举证为颜色矩阵。

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

\

\

那如果要改变一个像素的颜色效果,我们只需要将该矩阵与像素的颜色向量相乘就可以了, 形式如下:

\

使用矩阵的方式表达灰度函数

traverse(imageData11, ({ r, g, b, a }) => {
    // 对每个像素进行灰度化处理;
    // const v = 0.2126 * r + 0.7152 * g + 0.0722 * b;
    return transformColor([r, g, b, a], grayscale(1));

});

// transformColor 是用来处理举证叉乘的函数
export function transformColor(color: any, ...matrix: any) {
  const [r, g, b, a] = color;
  matrix = matrix.reduce((m1: any, m2: any) => multiply(m1, m2));
  color[0] =
    matrix[0] * r + matrix[1] * g + matrix[2] * b + matrix[3] * a + matrix[4];
  color[1] =
    matrix[5] * r + matrix[6] * g + matrix[7] * b + matrix[8] * a + matrix[9];
  color[2] =
    matrix[10] * r +
    matrix[11] * g +
    matrix[12] * b +
    matrix[13] * a +
    matrix[14];
  color[3] =
    matrix[15] * r +
    matrix[16] * g +
    matrix[17] * b +
    matrix[18] * a +
    matrix[19];
  return color;
}

灰度矩阵的表达

p 代表一个变量,0代表不完成灰度,也就是原色,1代码完成灰度。

export function grayscale(p) {
  p = clamp(0, 1, p);
  // console.log(p)
  const r = 0.212 * p;
  const g = 0.714 * p;
  const b = 0.074 * 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,
  ];
}

3.5 其他滤镜函数的矩阵表达方式

滤镜函数有很多,比如说:灰度化,亮度,饱和度,对比度,透明度,反色,色相旋转。接下来我们一一用矩阵的方式进行表达

3.6 亮度

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

3.7 饱和度

//饱和度,与grayscale正好相反
//p=0完全灰度化,p=1原色,p>1增强饱和度
export function saturate(p) {
  // p = clamp(0, 1, 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,
  ];
}

3.8 对比度

// 对比度,p=1原色,p<1减弱对比度,p>1增强对比度
export 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
  ];
}

3.9 透明度

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

3.10 反色

// 反色,p=0原色,p=1完全反色
export 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
  ];
}

3.11 色相旋转

//色相旋转,将色调沿极坐标转过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.14,
    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,
  ];
}

上面的滤镜效果是单一的滤镜效果,其实我们可以把滤镜函数叠加使用。那么如果进行叠加使用了?其实我们只要将这些滤镜函数返回的滤镜矩阵先相乘,然后把得到的矩阵再与输入的 RGBA 颜色向量相乘就可以了,如使用方法如下:

 traverse(imageData11, ({ r, g, b, a }) => {
    // 对每个像素进行灰度化处理;
    // const v = 0.2126 * r + 0.7152 * g + 0.0722 * b;
    return transformColor([r, g, b, a],
      brightness(1.2),// 增强亮度
      saturate(1.2) // 增强饱和度
    )

  });

4. 高斯模糊

高斯模糊的原理与颜色滤镜不同,高斯模糊不是单纯根据颜色矩阵计算当前像素点的颜色值,而是会按照高斯分布的权重,对当前像素点及其周围像素点的颜色按照高斯分布的权重加权平均。这样做,我们就能让图片各像素色值与周围色值的差异减小,从而达到平滑,或者说是模糊的效果。所以,高斯模糊是一个非常重要的平滑效果滤镜

高斯模糊的算法分两步,第一步是生成高斯分布矩阵,这个矩阵的作用是按照高斯函数提供平滑过程中参与计算的像素点的加权平均权重。代码如下:

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 };
}

由于高斯分布涉及比较专业的数学知识,我自己也没有研究明白,这里大家就知道高斯模糊,可以处理一个范围的数据就可以了,他可以达到磨平的效果\

5. CSS滤镜

熟悉css的通过应该知道css是支持滤镜的,就是filter,他支持很多滤镜效果,且可以叠加。

下面的图,是我从菜鸟教程截取下来的,是css支持滤镜的效果,与我们上面用矩阵实现的效果是一致的。

\

5.1 css滤镜叠加

CSS 也是可以实现多种滤镜效果跌倒的。

img {
    -webkit-filter: contrast(200%) brightness(150%);  /* Chrome, Safari, Opera */
    filter: contrast(200%) brightness(150%);
}

课后习题:

你能利用鼠标事件和今天学过的内容,做出一个图片局部“放大器”的效果吗?具体的效果就是,在鼠标移动在图片上时,将图片以鼠标坐标为圆心,指定半径内的内容局部放大。


致谢

如果感觉我的文章有用,请关注下我的公众号: 前端小黄。 我将不定期更新我的原创文章。

本文章源码地址:github.com/hpstream/We…