这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图像滤镜,如果你觉得有意思的话,点点关注点点赞吧~如果你有其他想法,欢迎评论区或私信交流。

相关文章
|
7月前
|
前端开发 JavaScript 程序员
程序员教你用代码制作圣诞树,正好圣诞节拿去送给女神给她个惊喜
使用HTML、CSS和JavaScript实现了一个圣诞树效果,包括一个闪烁的圣诞树和一个动态的光斑。代码包含一个&lt;div&gt;元素作为遮罩,一个&lt;canvas&gt;元素绘制星星动画,以及一个SVG元素绘制圣诞树。页面还包含一个提示用户先点赞再观看的提示。此效果适用于任何浏览器,推荐使用谷歌浏览器。提供了一段HTML代码,可以直接复制粘贴到文件中并以.html格式打开查看效果。
137 0
|
算法
【迎战蓝桥】 算法·每日一题(详解+多解)-- day5
💖1. 数组中出现次数超过一半的数字 💖2. 二进制中1的个数 💖3. 替换空格
105 0
【迎战蓝桥】 算法·每日一题(详解+多解)-- day5
每日一题——找出游戏的获胜者
每日一题——找出游戏的获胜者
104 0
每日一题——找出游戏的获胜者
|
算法 C++
【快乐手撕LeetCode题解系列】——消失的数字
哈喽各位友友们😊,我今天又学到了很多有趣的知识,现在迫不及待的想和大家分享一下!😘我仅已此文,和大家分享【快乐手撕LeetCode题解系列】——消失的数字~ 都是精华内容,可不要错过哟!!!😍😍😍
96 0
|
移动开发
【寒假每日一题】AcWing 4261. 孤独的照片(补)
文章目录 一、题目 1、原题链接 2、题目描述 二、解题报告 1、思路分析 2、时间复杂度 3、代码详解
87 0
|
存储 程序员
这个颜值逆天的姑娘,居然是一枚程序员!(多图慎入,内有彩蛋)
在2016年阿里云年会上,一位清纯美丽、身材高挑、健康阳光的“维秘天使”闪亮登场,令现场的阿里云汉子们按耐不住心中的激荡,惊呼“女神”驾到! 如今随着网络的传播,这位阿里云女神已经在IT圈掀起了小小的波澜,不仅是阿里人,很多技术小伙伴都加入了她的粉丝圈,纷纷询问,这位女神到底是谁?  今天,我们就
20725 0
|
程序员 Python
自学python一周,看我如何用python实现黑客帝国字母雨
自学python一周,看我如何用python实现黑客帝国字母雨
2325 0
|
程序员
一位40多岁骨灰级程序员的干货分享!
前段时间在极客时间上购买了 Service Mesh 课程,并在我们Java技术栈知识星球VIP微信群中进行了分享,提供给球友免费阅读。 今天,我又花了199元订阅了极客时间上一位大咖的技术专栏(左耳朵耗子),这真是个牛人,四十多岁的骨灰级程序员!看了下他的个人介绍很震撼,分享的技术知识体系也非常不错,现在特别推荐给大家。
1287 0