这10种图像滤镜是否让你想起一位故人

简介: 这10种图像滤镜是否让你想起一位故人

前言

大家好,我是练习时长一坤年的前端练习生,之前发布了一篇打造图像编辑器(一)——基础架构与图像滤镜介绍了图像编辑器的基础架构和基础的图像滤镜,我们今天接着来介绍一些更有意思的图像滤镜。

体验地址

image.png

Canvas操作像素

今天我们要实现的滤镜效果,都是通过操作每一个像素点,应用一些特殊的变换去实现的。首先我们先来看看在canvas中如何操作像素点。

const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
console.log("data", data);

通过getImageData这个API可以拿到画布对应的像素数据,data打印出来长成下面的这个样子。

image.png

data是一个一维数组,包含了每个像素的颜色信息。数组中的每四个元素表示一个像素的红、绿、蓝和透明度值(RGBA格式),取值范围是0255。因此,数组的长度是 width * height * 4。也就是说我们在操作像素点的时候一般会写出下面这样的代码:

for (let i = 0; i < data.length; i += 4) {
    const red = data[i];
    const green = data[i+1];
    const blue = data[i+2];
    const alpha = data[i+3];
}

上面用一个for循环去遍历data这个一维数组,步进长度是4,其中红绿蓝透明度就分别对应区间内的第0项、第1项、第2项和第3项。拿到一个个像素点的信息之后,我们就可以开始对每一个像素点进行变换操作:

  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    data[i] = 255;
    data[i + 1] = 255;
    data[i + 2] = 255;
  }
  ctx.clearRect(0, 0, width, height);
  ctx.putImageData(imageData, 0, 0);

操作完每一个像素点之后,通过putImageData把变换完的像素操作作用在画布上。

滤镜集合

介绍完了如何操作像素点之后,下面来介绍每一个滤镜的具体实现,今天实现的滤镜包括如下几个:

  • 黑白滤镜
  • 灰度滤镜
  • 怀旧滤镜
  • 连环画滤镜
  • 冷/暖色调滤镜
  • 油画滤镜
  • 素描滤镜
  • 水彩画滤镜
  • 马赛克滤镜
  • 模糊滤镜(高斯模糊)

黑白滤镜

要实现黑白滤镜,我们可以按照以下的步骤去操作:

  • 获取画布的图像区域并提取图像的像素信息
  • 设置阈值,用于判断像素是否应该被转换为黑色或白色
  • 计算当前像素的灰度值,将红、绿、蓝通道的平均值作为灰度值
  • 根据灰度值与阈值的比较,将像素设置为黑色(0)或白色(255
  • 把变更作用到画布上
const blackWhite = ({ ctx, width, height }) => {
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;
  const threshold = 128;
  for (let i = 0; i < data.length; i += 4) {
    const gray = (data[i] + data[i + 1] + data[i + 2]) / 3;
    const binaryValue = gray < threshold ? 0 : 255;
    data[i] = binaryValue;
    data[i + 1] = binaryValue;
    data[i + 2] = binaryValue;
  }
  ctx.clearRect(0, 0, width, height);
  ctx.putImageData(imageData, 0, 0);
};

image.png

灰度滤镜

灰度值是表示颜色亮度的单一值,通常用于将彩色图像转换为黑白或灰度图像。在彩色图像中,每个像素由红(R)、绿(G)、蓝(B)三个通道的颜色值组成。灰度值的计算方法可以通过对这三个颜色通道的值进行加权平均得到。

在灰度图像中,每个像素仅有一个单一的灰度值,表示该像素的亮度或强度。这个灰度值通常在0(黑色)到255(白色)的范围内,表示从最暗到最亮的不同亮度级别。

灰度效果是指通过将彩色图像中的颜色信息转换为灰度值,从而呈现出黑白或灰度的视觉效果。这种转换有助于简化图像,突出亮度和暗度的变化,使人们更关注图像的亮度和结构,而不受到颜色的干扰。

实现灰度滤镜,可以按照以下的步骤去实现:

  • 遍历每个像素的 RGBA 值,加权平均算出灰度值
  • 将灰度值作用在红绿蓝通道上
const gray = ({ ctx, width, height }) => {
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    const red = data[i];
    const green = data[i + 1];
    const blue = data[i + 2];
    const gray = 0.299 * red + 0.587 * green + 0.114 * blue;
    data[i] = gray;
    data[i + 1] = gray;
    data[i + 2] = gray;
  }
  ctx.clearRect(0, 0, width, height);
  ctx.putImageData(imageData, 0, 0);
};

image.png

怀旧滤镜

实现怀旧滤镜的方式也大差不差,核心的片段是:

const newRed = 0.393 * r + 0.769 * g + 0.189 * b;
const newGreen = 0.349 * r + 0.686 * g + 0.168 * b;
const newBlue = 0.272 * r + 0.534 * g + 0.131 * b;

根据旧的rgb值通过一个颜色矩阵变换,实现怀旧滤镜的效果。这个颜色矩阵产生一种较为暖色调的效果,通过使用这样的矩阵,可以减弱原始图像的鲜艳度,加深暖色调,从而使图像看起来更像是以前使用的胶片拍摄的照片。

const old = ({ ctx, width, height }) => {
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    const r = data[i];
    const g = data[i + 1];
    const b = data[i + 2];
    const newRed = 0.393 * r + 0.769 * g + 0.189 * b;
    const newGreen = 0.349 * r + 0.686 * g + 0.168 * b;
    const newBlue = 0.272 * r + 0.534 * g + 0.131 * b;
    data[i] = newRed;
    data[i + 1] = newGreen;
    data[i + 2] = newBlue;
  }
  ctx.clearRect(0, 0, width, height);
  ctx.putImageData(imageData, 0, 0);
};

image.png

连环画

实现连环画的核心片段是:

const newRed = (Math.abs(g - b + g + r) * r) / 256;
const newGreen = (Math.abs(b - g + b + r) * r) / 256;
const newBlue = (Math.abs(b - g + b + r) * g) / 256;

这里使用了对红、绿、蓝通道的差异计算,通过加减运算和取绝对值,增强了颜色通道之间的差异。总体上,这样的变换增强了图像中颜色的对比度,突出了图像的边缘,产生了一种具有连环画风格的效果,这些数学变换是通过试验和调整获得的。

const comics = ({ ctx, width, height }) => {
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    const r = data[i];
    const g = data[i + 1];
    const b = data[i + 2];
    const newRed = (Math.abs(g - b + g + r) * r) / 256;
    const newGreen = (Math.abs(b - g + b + r) * r) / 256;
    const newBlue = (Math.abs(b - g + b + r) * g) / 256;
    data[i] = newRed;
    data[i + 1] = newGreen;
    data[i + 2] = newBlue;
  }
  ctx.clearRect(0, 0, width, height);
  ctx.putImageData(imageData, 0, 0);
};

image.png

冷/暖色调

冷色调通常与较低的温度相关联,呈现出类似蓝色、绿色和紫色的色调;暖色调通常与较高的温度相关联,呈现出类似红色、橙色和黄色的色调。在RGB颜色模型中,调整冷暖色调一般是调整红蓝通道:

  • 若要增强冷色调,可以增加蓝色通道的强度,减小红色通道的强度。
  • 若要增强暖色调,可以减小蓝色通道的强度,增加红色通道的强度。
const color = ({ ctx, width, height, value }) => {
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    const red = data[i];
    const green = data[i + 1];
    const blue = data[i + 2];
    data[i] = Math.max(0, red + value);
    data[i + 1] = green;
    data[i + 2] = Math.min(255, blue - value);
  }
  ctx.clearRect(0, 0, width, height);
  ctx.putImageData(imageData, 0, 0);
};

冷色调:

image.png

暖色调:

image.png

油画

对于油画滤镜的实现,可以分为以下的步骤:

  • 计算像素周围一定半径内的像素颜色平均值,得到一个更加模糊、平滑的颜色,这样可以模拟油画中的画笔过滤效果。
  • 将颜色映射到一个有限的离散范围,模拟油画中明显的颜色变化和层次,模拟手绘油画的效果

通过颜色平均和离散化的处理,使图像呈现出一种油画般的柔和和层次感,模拟油画的绘图感觉

const oilPaint = ({ ctx, width, height }) => {
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;

  const radius = 2; // 滤波半径
  const levels = 10; // 离散层次数

  for (let y = 0; y < height; y += 1) {
    for (let x = 0; x < width; x += 1) {
      const i = (y * width + x) * 4;

      // 计算滤波半径内的颜色平均值
      let totalRed = 0;
      let totalGreen = 0;
      let totalBlue = 0;
      let count = 0;

      for (let dy = -radius; dy <= radius; dy += 1) {
        for (let dx = -radius; dx <= radius; dx += 1) {
          const nx = Math.min(width - 1, Math.max(0, x + dx));
          const ny = Math.min(height - 1, Math.max(0, y + dy));
          const ni = (ny * width + nx) * 4;

          totalRed += data[ni];
          totalGreen += data[ni + 1];
          totalBlue += data[ni + 2];
          count += 1;
        }
      }

      // 计算平均值并应用到当前像素
      const avgRed = totalRed / count;
      const avgGreen = totalGreen / count;
      const avgBlue = totalBlue / count;

      // 将颜色离散化到指定的层次数
      const discreteRed =
        (Math.round((avgRed / 255) * (levels - 1)) / (levels - 1)) * 255;
      const discreteGreen =
        (Math.round((avgGreen / 255) * (levels - 1)) / (levels - 1)) * 255;
      const discreteBlue =
        (Math.round((avgBlue / 255) * (levels - 1)) / (levels - 1)) * 255;

      data[i] = discreteRed;
      data[i + 1] = discreteGreen;
      data[i + 2] = discreteBlue;
    }
  }

  ctx.clearRect(0, 0, width, height);
  ctx.putImageData(imageData, 0, 0);
};

image.png

素描滤镜

通过调整图像中每个像素的颜色以强调其轮廓和明暗关系,可以模拟铅笔素描的视觉效果。具体可以通过以下方式实现,跟灰度滤镜差不多:

  • 计算每个像素的红、绿、蓝通道的平均值,得到一个灰度值。
  • 计算每个像素颜色值与平均灰度值的梯度。梯度表示颜色相对于平均灰度的变化程度,即颜色相对于周围区域的明暗程度。
  • 将梯度乘以一个权重值,然后将结果加回到平均灰度值上。
const sketch = ({ ctx, width, height }) => {
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;

  for (let i = 0; i < data.length; i += 4) {
    const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
    const gradient = data[i] - avg;

    // 梯度权重
    const weight = 1;
    data[i] = avg + gradient * weight;
    data[i + 1] = avg + gradient * weight;
    data[i + 2] = avg + gradient * weight;
  }
  ctx.clearRect(0, 0, width, height);
  ctx.putImageData(imageData, 0, 0);
};

image.png

水彩画滤镜

  • 通过在半径范围内随机选取一个像素的颜色来替换当前像素的颜色,模拟水彩画中颜色的扩散和混合效果。
  • 在局部区域内实现颜色的混合,营造出水彩画般的柔和和自然的效果。
const waterColor = ({ ctx, width, height }) => {
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;

  const radius = 5; // 水彩画效果的半径

  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let randomX =
        x + Math.floor(Math.random() * radius) - Math.floor(radius / 2);
      let randomY =
        y + Math.floor(Math.random() * radius) - Math.floor(radius / 2);

      // 边界检查
      randomX = Math.max(0, Math.min(width - 1, randomX));
      randomY = Math.max(0, Math.min(height - 1, randomY));

      const i = (y * width + x) * 4;
      const randomI = (randomY * width + randomX) * 4;

      data[i] = data[randomI];
      data[i + 1] = data[randomI + 1];
      data[i + 2] = data[randomI + 2];
      data[i + 3] = data[randomI + 3];
    }
  }
  ctx.clearRect(0, 0, width, height);
  ctx.putImageData(imageData, 0, 0);
};

image.png

马赛克滤镜

马赛克滤镜的核心思想是将图像分成块状区域,每个块内的像素颜色都设置为该块内的第一个像素的颜色。这样的操作可以让图像中相邻像素的颜色变得相同,产生了一种像素化马赛克的效果。

const mosaic = ({ ctx, width, height }) => {
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;
  const blockSize = 10;
  for (let y = 0; y < height; y += blockSize) {
    for (let x = 0; x < width; x += blockSize) {
      const index = (y * width + x) * 4;
      const red = data[index];
      const green = data[index + 1];
      const blue = data[index + 2];

      // 将当前块内的所有像素颜色设置为第一个像素的颜色
      for (let i = 0; i < blockSize; i++) {
        for (let j = 0; j < blockSize; j++) {
          const blockIndex = ((y + i) * width + (x + j)) * 4;
          data[blockIndex] = red;
          data[blockIndex + 1] = green;
          data[blockIndex + 2] = blue;
        }
      }
    }
  }

  ctx.clearRect(0, 0, width, height);
  ctx.putImageData(imageData, 0, 0);
};

image.png

模糊滤镜

  • 这里使用的模糊算法是高斯模糊,通过定义一个模糊半径生成一个高斯核
  • 计算每个像素在半径范围内的加权平均值实现高斯模糊
const blur = ({ ctx, width, height }) => {
  const radius = 10;
  const generateGaussianKernel = (radius) => {
    const size = radius * 2 + 1;
    const kernel = [];

    const sigma = radius / 3;
    const sigmaSquared = 2 * sigma * sigma;

    let sum = 0;

    for (let i = -radius; i <= radius; i++) {
      const row = [];
      for (let j = -radius; j <= radius; j++) {
        const distanceSquared = i * i + j * j;
        const weight =
          Math.exp(-distanceSquared / sigmaSquared) / (Math.PI * sigmaSquared);
        row.push(weight);
        sum += weight;
      }
      kernel.push(row);
    }

    // 归一化
    for (let i = 0; i < size; i++) {
      for (let j = 0; j < size; j++) {
        kernel[i][j] /= sum;
      }
    }

    return kernel;
  };
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;
  // 创建一个临时数组来保存原始图像数据
  const originalData = new Uint8ClampedArray(data);
  // 计算高斯核心权重
  const kernel = generateGaussianKernel(radius);
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let sumRed = 0;
      let sumGreen = 0;
      let sumBlue = 0;

      for (let i = -radius; i <= radius; i++) {
        for (let j = -radius; j <= radius; j++) {
          let currentX = x + j;
          let currentY = y + i;

          // 边缘检测
          currentX = Math.min(width - 1, Math.max(0, currentX));
          currentY = Math.min(height - 1, Math.max(0, currentY));

          let index = (currentY * width + currentX) * 4;
          let weight = kernel[i + radius][j + radius];

          sumRed += originalData[index] * weight;
          sumGreen += originalData[index + 1] * weight;
          sumBlue += originalData[index + 2] * weight;
        }
      }

      let currentIndex = (y * width + x) * 4;

      data[currentIndex] = sumRed;
      data[currentIndex + 1] = sumGreen;
      data[currentIndex + 2] = sumBlue;
    }
  }

  ctx.clearRect(0, 0, width, height);
  ctx.putImageData(imageData, 0, 0);
};

image.png

最后

以上就是本文实现的10canvas图像滤镜,如果你觉得有意思的话,点点关注点点赞吧~如果你有其他想法,欢迎评论区或私信交流。

相关文章
|
6月前
|
存储 人工智能 JavaScript
编织魔法与修电脑:码农征途的奇妙起点
编织魔法与修电脑:码农征途的奇妙起点
80 0
|
6月前
|
前端开发 JavaScript 程序员
程序员教你用代码制作圣诞树,正好圣诞节拿去送给女神给她个惊喜
使用HTML、CSS和JavaScript实现了一个圣诞树效果,包括一个闪烁的圣诞树和一个动态的光斑。代码包含一个&lt;div&gt;元素作为遮罩,一个&lt;canvas&gt;元素绘制星星动画,以及一个SVG元素绘制圣诞树。页面还包含一个提示用户先点赞再观看的提示。此效果适用于任何浏览器,推荐使用谷歌浏览器。提供了一段HTML代码,可以直接复制粘贴到文件中并以.html格式打开查看效果。
74 0
|
数据采集 Web App开发 XML
干了这碗“美丽汤”,网页解析倍儿爽
HTML 文档本身是结构化的文本,有一定的规则,通过它的结构可以简化信息提取。于是,就有了lxml、pyquery、BeautifulSoup等网页信息提取库。一般我们会用这些库来提取网页信息。
|
数据采集 数据挖掘 程序员
【每周一坑】程序猿的浪漫
长久以来,大家对程序员的印象是“呆板”、”内向”等,殊不知他们也有浪漫的一面。把找不到对象归因于职业性质,这个锅,面向对象的编程语言不背!(但这个报道真不是来黑程序员的吗……)
|
小程序 数据安全/隐私保护 计算机视觉
切勿外传,我要把我的写作“小心思”放出来了!| 年终总结之学习篇🚩
切勿外传,我要把我的写作“小心思”放出来了!| 年终总结之学习篇🚩
177 0
切勿外传,我要把我的写作“小心思”放出来了!| 年终总结之学习篇🚩
|
前端开发
前端百题斩【018】——从验证点到手撕new操作符
前端百题斩【018】——从验证点到手撕new操作符
前端百题斩【018】——从验证点到手撕new操作符
|
算法 搜索推荐
对于蓝桥你不得不知的应试技巧
本文适用于蓝桥杯算法比赛赛前使用
312 0
对于蓝桥你不得不知的应试技巧
|
程序员
程序猿最爱说的假话,你中枪了么?(测测你是哪种类型的谎话精)
你还没参加程序员“真”心话接龙挑战? 部门同事都试过啦, 就差你了! 猜猜看,你们会不会集齐所有类型,召唤神龙呢?
1062 0
程序猿最爱说的假话,你中枪了么?(测测你是哪种类型的谎话精)
|
C语言 C++
小伙子用C语言写出绽放的玫瑰花,成功表白C++代码女神!
小伙子用C语言写出绽放的玫瑰花,成功表白C++代码女神!
5160 0