Canvas图像处理

124 阅读5分钟

Canvas图像处理


原图

1. 图像滤镜效果

灰度化

function grayscaleImage(imageData) {  
  const data = imageData.data;  
  for (let i = 0; i < data.length; i += 4) {  
    const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;  
    data[i] = avg; // red  
    data[i + 1] = avg; // green  
    data[i + 2] = avg; // blue  
  }  
  return imageData;  
}  
  
// 应用灰度效果  
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);  
const grayImageData = grayscaleImage(imageData);  
ctx.putImageData(grayImageData, 0, 0);

灰度化

老照片

function getHue(imageData) {  
  const data = imageData.data;  
  for (var i = 0; i < data.length; i += 4) {
		const r = data[i + 0]
		const g = data[i + 1]
		const b = data[i + 2]

		data[i + 0] = r * 0.28 + g * 0.72 + b * 0.22
		data[i + 1] = r * 0.25 + g * 0.63 + b * 0.13
		data[i + 2] = r * 0.17 + g * 0.66 + b * 0.13
	}
  return imageData;  
}  
  
// 应用老照片效果  
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);  
const grayImageData = getHue(imageData);  
ctx.putImageData(grayImageData, 0, 0);

老照片

模糊化

模糊化通常涉及更复杂的像素操作,可以使用Web Workers进行后台处理,避免阻塞UI线程。这里只提供简单的模糊概念代码片段。

function getBlur(imageData) {
        const data = imageData.data;
        let width = imageData.width;
        let height = imageData.height;
        const gaussMatrix = [];

        let gaussSum = 0;
        let x = 0;
        let y = 0;

        let i = 0;
        let j = 0;
        let k = 0;
        let len = 0;

        const radius = 10;
        const sigma = 8;

        let r = 0;
        let g = 0;
        let b = -1 / (2 * sigma * sigma);
        let a = 1 / (Math.sqrt(2 * Math.PI) * sigma);

        // 生成高斯矩阵
        for (i = 0, x = -radius; x <= radius; x++, i++) {
          g = a * Math.exp(b * x * x);
          gaussMatrix[i] = g;
          gaussSum += g;
        }

        // 归一化, 保证高斯矩阵的值在[0,1]之间
        for (i = 0, len = gaussMatrix.length; i < len; i++) {
          gaussMatrix[i] /= gaussSum;
        }

        // x 方向一维高斯运算
        for (y = 0; y < height; y++) {
          for (x = 0; x < width; x++) {
            r = g = b = a = 0;
            gaussSum = 0;
            for (j = -radius; j <= radius; j++) {
              k = x + j;
              if (k >= 0 && k < width) {
                // 确保 k 没超出 x 的范围
                // r,g,b,a 四个一组
                i = (y * width + k) * 4;
                r += data[i] * gaussMatrix[j + radius];
                g += data[i + 1] * gaussMatrix[j + radius];
                b += data[i + 2] * gaussMatrix[j + radius];
                gaussSum += gaussMatrix[j + radius];
              }
            }
            i = (y * width + x) * 4;
            // 除以 gaussSum 是为了消除处于边缘的像素, 高斯运算不足的问题
            // console.log(gaussSum)
            data[i] = r / gaussSum;
            data[i + 1] = g / gaussSum;
            data[i + 2] = b / gaussSum;
          }
        }

        // y 方向一维高斯运算
        for (x = 0; x < width; x++) {
          for (y = 0; y < height; y++) {
            r = g = b = a = 0;
            gaussSum = 0;
            for (j = -radius; j <= radius; j++) {
              k = y + j;
              if (k >= 0 && k < height) {
                // 确保 k 没超出 y 的范围
                i = (k * width + x) * 4;
                r += data[i] * gaussMatrix[j + radius];
                g += data[i + 1] * gaussMatrix[j + radius];
                b += data[i + 2] * gaussMatrix[j + radius];
                gaussSum += gaussMatrix[j + radius];
              }
            }
            i = (y * width + x) * 4;
            data[i] = r / gaussSum;
            data[i + 1] = g / gaussSum;
            data[i + 2] = b / gaussSum;
          }
        }
        return imageData;
      }
  
// 应用老照片效果  
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);  
const grayImageData = getBlur(imageData);  
ctx.putImageData(grayImageData, 0, 0);

模糊化

马赛克效果

function getCodeFeel(imageData) {
        const data = imageData.data;
        let width = imageData.width;
        let height = imageData.height;
        const blur = 4; // 马赛克块宽度
        const blurR = 2 * blur + 1;
        const total = blurR * blurR;

        for (let i = blur; i <= width; i = i + blurR) {
          for (let j = blur; j <= height; j = j + blurR) {
            let r = 0;
            let g = 0;
            let b = 0;
            for (let leny = -blur; leny <= blur; leny++) {
              for (let lenx = -blur; lenx <= blur; lenx++) {
                r += data[4 * ((j + leny) * width + i + lenx) + 0];
                g += data[4 * ((j + leny) * width + i + lenx) + 1];
                b += data[4 * ((j + leny) * width + i + lenx) + 2];
              }
            }

            let vr = r / total;
            let vg = g / total;
            let vb = b / total;
            for (let leny = -blur; leny <= blur; leny++) {
              for (let lenx = -blur; lenx <= blur; lenx++) {
                data[4 * ((j + leny) * width + i + lenx) + 0] = vr;
                data[4 * ((j + leny) * width + i + lenx) + 1] = vg;
                data[4 * ((j + leny) * width + i + lenx) + 2] = vb;
              }
            }
          }
        }
        return imageData;
      }
  
// 应用马赛克效果  
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);  
const grayImageData = getCodeFeel(imageData);  
ctx.putImageData(grayImageData, 0, 0);

马赛克

反转

function invertImage(imageData) {  
  const data = imageData.data;  
  for (let i = 0; i < data.length; i += 4) {  
    data[i] = 255 - data[i]; // red  
    data[i + 1] = 255 - data[i + 1]; // green  
    data[i + 2] = 255 - data[i + 2]; // blue  
  }  
  return imageData;  
}  
  
// 应用反转效果  
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);  
const invertedImageData = invertImage(imageData);  
ctx.putImageData(invertedImageData, 0, 0);

2. 图像合成

Canvas 2D API 的 CanvasRenderingContext2D.globalCompositeOperation 属性设置要在绘制新形状时应用的合成操作的类型。通过改变globalCompositeOperation属性,可以实现各种图像合成效果。

"source-over"

这是默认设置,并在现有画布上绘制新图形。

"source-in"

仅在新形状和目标画布重叠的地方绘制新形状。其他的都是透明的。

"source-out"

在不与现有画布内容重叠的地方绘制新图形。

"source-atop"

只在与现有画布内容重叠的地方绘制新图形。

"destination-over"

在现有画布内容的后面绘制新的图形。

"destination-in"

仅保留现有画布内容和新形状重叠的部分。其他的都是透明的。

"destination-out"

仅保留现有画布内容和新形状不重叠的部分。

"destination-atop"

仅保留现有画布内容和新形状重叠的部分。新形状是在现有画布内容的后面绘制的。

"lighter"

两个重叠图形的颜色是通过颜色值相加来确定的。

"copy"

只显示新图形。

"xor"

形状在重叠处变为透明,并在其他地方正常绘制。

"multiply"

将顶层像素与底层相应像素相乘,结果是一幅更黑暗的图片。

"screen"

像素被倒转、相乘、再倒转,结果是一幅更明亮的图片(与 ****multiply ****相反)。

"overlay"

multiply **** ****screen ****的结合。原本暗的地方更暗,原本亮的地方更亮。

"darken"

保留两个图层中最暗的像素。

"lighten"

保留两个图层中最亮的像素。

"color-dodge"

将底层除以顶层的反置。

"color-burn"

将反置的底层除以顶层,然后将结果反过来。

"hard-light"

类似于 ****overlay multiply **** ****screen ****的结合——但上下图层互换了。

"soft-light"

柔和版本的 ****hard-light 。纯黑或纯白不会导致纯黑或纯白。

"difference"

从顶层减去底层(或反之亦然),始终得到正值。

"exclusion"

****difference ****类似,但对比度较低。

"hue"

保留底层的亮度(luma)和色度(chroma),同时采用顶层的色调(hue)。

"saturation"

保留底层的亮度和色调,同时采用顶层的色度。

"color"

保留了底层的亮度,同时采用了顶层的色调和色度。

"luminosity"

保持底层的色调和色度,同时采用顶层的亮度。

ctx.globalCompositeOperation = 'multiply'; // 将顶层像素与底层相应像素相乘  
ctx.drawImage(img2, 0, 0); // img2

与画布上的现有内容以乘法模式合成。

multiply-合成后

lighter-合成后

overlay-合成后

优化与性能提升

图像资源管理

const imageCache = {}; // 图像缓存对象  
  
function loadImage(src, callback) {  
  if (imageCache[src]) {  
    callback(imageCache[src]);  
    return;  
  }  
  const img = new Image();  
  img.onload = function() {  
    imageCache[src] = img; // 缓存图像  
    callback(img);  
  };  
  img.src = src;  
}

Canvas性能优化技巧

  • 尽量减少重绘次数,通过offscreenCanvas进行离屏绘制,再一次性绘制到主Canvas上。
  • 合并绘制操作,如使用beginPath和closePath来减少路径绘制的次数。
  • 避免在循环中频繁调用绘图方法,而是预先计算好所有需要的值,再一次性绘制。

移动端适配

// 响应式设计  
const canvas = document.getElementById('myCanvas');  
const dpr = window.devicePixelRatio || 1;  
canvas.width = canvas.offsetWidth * dpr;  
canvas.height = canvas.offsetHeight * dpr;  
ctx.scale(dpr, dpr);  
  
// 触控事件处理  
canvas.addEventListener('touchstart', handleTouchStart, false);  
canvas.addEventListener('touchmove', handleTouchMove, false);