pin_drop当前位置:知识文库 ❯ 图文

多媒体:像素操作 - 完整教程与代码示例

一、教程简介

Canvas 像素操作允许开发者直接读写画布上的每一个像素数据,这是实现图像滤镜、颜色调整、图像合成等高级效果的基础。通过 getImageData 获取像素数据,通过 putImageData 写回像素数据,配合 createImageData 创建空白图像数据,可以实现各种图像处理效果。本教程将详细讲解像素操作的原理、方法和实际应用。


二、核心概念

ImageData 对象

ImageData 对象包含三个属性:

属性 说明
width 图像数据宽度
height 图像数据高度
data Uint8ClampedArray,包含 RGBA 像素数据

像素数据格式

data 数组中每 4 个元素表示一个像素:

代码示例

[data[0], data[1], data[2], data[3]] = [R, G, B, A]
  • R:红色通道(0~255)

  • G:绿色通道(0~255)

  • B:蓝色通道(0~255)

  • A:透明度通道(0~255,0 完全透明,255 完全不透明)

像素索引计算

对于坐标 (x, y) 的像素,在 data 数组中的起始索引为:

代码示例

const index = (y * imageData.width + x) * 4;

三、语法与用法

getImageData - 获取像素数据

代码示例

const imageData = ctx.getImageData(sx, sy, sw, sh);
参数 说明
sx, sy 起始坐标
sw, sh 宽度和高度

putImageData - 写回像素数据

代码示例

ctx.putImageData(imageData, dx, dy);
ctx.putImageData(imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight);
参数 说明
imageData ImageData 对象
dx, dy 目标位置坐标
dirtyX, dirtyY, dirtyWidth, dirtyHeight 可选,脏矩形区域

createImageData - 创建空白图像数据

代码示例

const imageData = ctx.createImageData(width, height);
const imageData = ctx.createImageData(anotherImageData);

四、代码示例

示例1:像素操作与图像滤镜

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>像素操作与图像滤镜</title>
  <style>
    body {
      margin: 0;
      padding: 20px;
      background: #0d1117;
      color: #c9d1d9;
      font-family: 'Segoe UI', sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    canvas {
      border: 1px solid #30363d;
      border-radius: 8px;
    }
    .filters {
      display: flex;
      gap: 8px;
      margin-top: 10px;
      flex-wrap: wrap;
      justify-content: center;
    }
    button {
      padding: 6px 14px;
      border: 1px solid #30363d;
      border-radius: 4px;
      background: #21262d;
      color: #c9d1d9;
      cursor: pointer;
      font-size: 13px;
    }
    button:hover { background: #30363d; }
    button.active { background: #58a6ff; color: #fff; border-color: #58a6ff; }
  </style>
</head>
<body>
  <h1>像素操作与图像滤镜</h1>
  <canvas id="myCanvas" width="700" height="350"></canvas>
  <div class="filters">
    <button class="active" onclick="applyFilter('original')">原图</button>
    <button onclick="applyFilter('grayscale')">灰度</button>
    <button onclick="applyFilter('invert')">反色</button>
    <button onclick="applyFilter('sepia')">复古</button>
    <button onclick="applyFilter('brightness')">增亮</button>
    <button onclick="applyFilter('contrast')">高对比</button>
    <button onclick="applyFilter('blur')">模糊</button>
    <button onclick="applyFilter('emboss')">浮雕</button>
  </div>

  <script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');

    // 绘制原始图像(使用程序化生成的彩色图案)
    function drawOriginal() {
      // 渐变背景
      const grad = ctx.createLinearGradient(0, 0, 700, 350);
      grad.addColorStop(0, '#e94560');
      grad.addColorStop(0.3, '#f8b500');
      grad.addColorStop(0.6, '#3fb950');
      grad.addColorStop(1, '#58a6ff');
      ctx.fillStyle = grad;
      ctx.fillRect(0, 0, 700, 350);

      // 绘制一些图形
      ctx.fillStyle = '#ffffff';
      ctx.font = 'bold 48px sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText('Pixel Art', 350, 100);

      ctx.beginPath();
      ctx.arc(200, 220, 50, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(255,255,255,0.5)';
      ctx.fill();

      ctx.beginPath();
      ctx.arc(500, 220, 50, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(0,0,0,0.3)';
      ctx.fill();

      // 小方块
      for (let i = 0; i < 20; i++) {
        const x = Math.random() * 660 + 20;
        const y = Math.random() * 310 + 20;
        const s = Math.random() * 20 + 5;
        ctx.fillStyle = `hsla(${Math.random() * 360}, 70%, 60%, 0.6)`;
        ctx.fillRect(x, y, s, s);
      }
    }

    drawOriginal();
    const originalData = ctx.getImageData(0, 0, 700, 350);

    // 滤镜函数
    const filters = {
      original() {
        ctx.putImageData(originalData, 0, 0);
      },

      grayscale() {
        const data = new ImageData(
          new Uint8ClampedArray(originalData.data),
          originalData.width, originalData.height
        );
        const d = data.data;
        for (let i = 0; i < d.length; i += 4) {
          const avg = d[i] * 0.299 + d[i+1] * 0.587 + d[i+2] * 0.114;
          d[i] = d[i+1] = d[i+2] = avg;
        }
        ctx.putImageData(data, 0, 0);
      },

      invert() {
        const data = new ImageData(
          new Uint8ClampedArray(originalData.data),
          originalData.width, originalData.height
        );
        const d = data.data;
        for (let i = 0; i < d.length; i += 4) {
          d[i] = 255 - d[i];
          d[i+1] = 255 - d[i+1];
          d[i+2] = 255 - d[i+2];
        }
        ctx.putImageData(data, 0, 0);
      },

      sepia() {
        const data = new ImageData(
          new Uint8ClampedArray(originalData.data),
          originalData.width, originalData.height
        );
        const d = data.data;
        for (let i = 0; i < d.length; i += 4) {
          const r = d[i], g = d[i+1], b = d[i+2];
          d[i]   = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189);
          d[i+1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168);
          d[i+2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131);
        }
        ctx.putImageData(data, 0, 0);
      },

      brightness() {
        const data = new ImageData(
          new Uint8ClampedArray(originalData.data),
          originalData.width, originalData.height
        );
        const d = data.data;
        const factor = 50;
        for (let i = 0; i < d.length; i += 4) {
          d[i]   = Math.min(255, d[i] + factor);
          d[i+1] = Math.min(255, d[i+1] + factor);
          d[i+2] = Math.min(255, d[i+2] + factor);
        }
        ctx.putImageData(data, 0, 0);
      },

      contrast() {
        const data = new ImageData(
          new Uint8ClampedArray(originalData.data),
          originalData.width, originalData.height
        );
        const d = data.data;
        const factor = 1.5;
        for (let i = 0; i < d.length; i += 4) {
          d[i]   = Math.min(255, Math.max(0, factor * (d[i] - 128) + 128));
          d[i+1] = Math.min(255, Math.max(0, factor * (d[i+1] - 128) + 128));
          d[i+2] = Math.min(255, Math.max(0, factor * (d[i+2] - 128) + 128));
        }
        ctx.putImageData(data, 0, 0);
      },

      blur() {
        const w = originalData.width, h = originalData.height;
        const src = originalData.data;
        const data = new ImageData(
          new Uint8ClampedArray(src), w, h
        );
        const d = data.data;
        const radius = 3;
        for (let y = radius; y < h - radius; y++) {
          for (let x = radius; x < w - radius; x++) {
            let r = 0, g = 0, b = 0, count = 0;
            for (let dy = -radius; dy <= radius; dy++) {
              for (let dx = -radius; dx <= radius; dx++) {
                const idx = ((y + dy) * w + (x + dx)) * 4;
                r += src[idx]; g += src[idx+1]; b += src[idx+2];
                count++;
              }
            }
            const idx = (y * w + x) * 4;
            d[idx] = r / count;
            d[idx+1] = g / count;
            d[idx+2] = b / count;
          }
        }
        ctx.putImageData(data, 0, 0);
      },

      emboss() {
        const w = originalData.width, h = originalData.height;
        const src = originalData.data;
        const data = new ImageData(
          new Uint8ClampedArray(src), w, h
        );
        const d = data.data;
        // 浮雕卷积核
        const kernel = [-2, -1, 0, -1, 1, 1, 0, 1, 2];
        for (let y = 1; y < h - 1; y++) {
          for (let x = 1; x < w - 1; x++) {
            let r = 0, g = 0, b = 0, ki = 0;
            for (let dy = -1; dy <= 1; dy++) {
              for (let dx = -1; dx <= 1; dx++) {
                const idx = ((y + dy) * w + (x + dx)) * 4;
                r += src[idx] * kernel[ki];
                g += src[idx+1] * kernel[ki];
                b += src[idx+2] * kernel[ki];
                ki++;
              }
            }
            const idx = (y * w + x) * 4;
            d[idx]   = Math.min(255, Math.max(0, r + 128));
            d[idx+1] = Math.min(255, Math.max(0, g + 128));
            d[idx+2] = Math.min(255, Math.max(0, b + 128));
          }
        }
        ctx.putImageData(data, 0, 0);
      }
    };

    function applyFilter(name) {
      document.querySelectorAll('button').forEach(b => b.classList.remove('active'));
      event.target.classList.add('active');
      filters[name]();
    }
  </script>
</body>
</html>

示例2:像素级绘图与颜色拾取

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>像素级绘图与颜色拾取</title>
  <style>
    body {
      margin: 0;
      padding: 20px;
      background: #1a1a2e;
      color: #eee;
      font-family: 'Segoe UI', sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    canvas {
      border: 1px solid #333;
      border-radius: 8px;
      cursor: crosshair;
    }
    .info {
      margin-top: 10px;
      display: flex;
      gap: 20px;
      align-items: center;
    }
    .color-preview {
      width: 40px;
      height: 40px;
      border: 2px solid #333;
      border-radius: 4px;
    }
  </style>
</head>
<body>
  <h1>像素级绘图与颜色拾取</h1>
  <canvas id="myCanvas" width="700" height="400"></canvas>
  <div class="info">
    <div class="color-preview" id="colorPreview" style="background:#000;"></div>
    <span id="colorInfo">点击画布拾取颜色</span>
  </div>

  <script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');

    // 绘制彩色渐变背景
    for (let y = 0; y < canvas.height; y++) {
      for (let x = 0; x < canvas.width; x += 4) {
        const r = Math.floor((x / canvas.width) * 255);
        const g = Math.floor((y / canvas.height) * 255);
        const b = Math.floor(((canvas.width - x) / canvas.width) * 255);
        ctx.fillStyle = `rgb(${r},${g},${b})`;
        ctx.fillRect(x, y, 4, 1);
      }
    }

    // 颜色拾取
    canvas.addEventListener('click', function(e) {
      const rect = canvas.getBoundingClientRect();
      const scaleX = canvas.width / rect.width;
      const scaleY = canvas.height / rect.height;
      const x = Math.floor((e.clientX - rect.left) * scaleX);
      const y = Math.floor((e.clientY - rect.top) * scaleY);

      const pixel = ctx.getImageData(x, y, 1, 1).data;
      const r = pixel[0], g = pixel[1], b = pixel[2], a = pixel[3];
      const hex = '#' + [r, g, b].map(v => v.toString(16).padStart(2, '0')).join('');

      document.getElementById('colorPreview').style.background = hex;
      document.getElementById('colorInfo').textContent =
        `RGB(${r}, ${g}, ${b}) | ${hex} | Alpha: ${a} | 坐标: (${x}, ${y})`;
    });

    // 鼠标移动时实时预览
    canvas.addEventListener('mousemove', function(e) {
      const rect = canvas.getBoundingClientRect();
      const scaleX = canvas.width / rect.width;
      const scaleY = canvas.height / rect.height;
      const x = Math.floor((e.clientX - rect.left) * scaleX);
      const y = Math.floor((e.clientY - rect.top) * scaleY);

      if (x >= 0 && x < canvas.width && y >= 0 && y < canvas.height) {
        const pixel = ctx.getImageData(x, y, 1, 1).data;
        const hex = '#' + [pixel[0], pixel[1], pixel[2]].map(v => v.toString(16).padStart(2, '0')).join('');
        document.getElementById('colorPreview').style.background = hex;
      }
    });
  </script>
</body>
</html>

五、浏览器兼容性

方法 Chrome Firefox Safari Edge IE
getImageData 2+ 2+ 4+ 12+ 9+
putImageData 2+ 2+ 4+ 12+ 9+
createImageData 2+ 2+ 4+ 12+ 9+

注意:跨域图片会导致 getImageData 抛出安全异常。


六、注意事项与最佳实践

1. 跨域安全限制

代码示例

// 加载跨域图片时设置 crossOrigin
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = 'https://example.com/image.png';
img.onload = function() {
  ctx.drawImage(img, 0, 0);
  const data = ctx.getImageData(0, 0, canvas.width, canvas.height); // 安全
};

2. 像素操作性能

代码示例

// 不推荐:频繁调用 getImageData
for (let x = 0; x < width; x++) {
  for (let y = 0; y < height; y++) {
    const pixel = ctx.getImageData(x, y, 1, 1); // 极慢
  }
}

// 推荐:一次获取所有像素
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
  // 操作像素
}
ctx.putImageData(imageData, 0, 0);

3. Uint8ClampedArray 自动钳位

代码示例

const data = imageData.data;
// 值自动钳位到 0~255
data[i] = -10;   // 变为 0
data[i] = 300;   // 变为 255
data[i] = 128.7; // 变为 129(四舍五入)

4. 创建 ImageData 副本

代码示例

// 方案1:使用构造函数
const copy = new ImageData(
  new Uint8ClampedArray(originalData.data),
  originalData.width,
  originalData.height
);

// 方案2:手动复制
const copy = ctx.createImageData(originalData);
copy.data.set(originalData.data);

5. putImageData 不受变换影响

代码示例

ctx.translate(100, 100);
ctx.putImageData(imageData, 0, 0); // 不受 translate 影响,仍在 (0,0)

七、代码规范示例

代码示例

// 推荐:封装像素操作
function getPixel(imageData, x, y) {
  const index = (y * imageData.width + x) * 4;
  return {
    r: imageData.data[index],
    g: imageData.data[index + 1],
    b: imageData.data[index + 2],
    a: imageData.data[index + 3]
  };
}

function setPixel(imageData, x, y, r, g, b, a) {
  const index = (y * imageData.width + x) * 4;
  imageData.data[index] = r;
  imageData.data[index + 1] = g;
  imageData.data[index + 2] = b;
  imageData.data[index + 3] = a !== undefined ? a : 255;
}

// 推荐:通用卷积滤镜
function applyConvolution(imageData, kernel, divisor) {
  const w = imageData.width, h = imageData.height;
  const src = new Uint8ClampedArray(imageData.data);
  const dst = imageData.data;
  const kSize = Math.sqrt(kernel.length) | 0;
  const half = (kSize / 2) | 0;
  divisor = divisor || 1;

  for (let y = half; y < h - half; y++) {
    for (let x = half; x < w - half; x++) {
      let r = 0, g = 0, b = 0, ki = 0;
      for (let ky = -half; ky <= half; ky++) {
        for (let kx = -half; kx <= half; kx++) {
          const idx = ((y + ky) * w + (x + kx)) * 4;
          r += src[idx] * kernel[ki];
          g += src[idx + 1] * kernel[ki];
          b += src[idx + 2] * kernel[ki];
          ki++;
        }
      }
      const idx = (y * w + x) * 4;
      dst[idx] = Math.min(255, Math.max(0, r / divisor));
      dst[idx + 1] = Math.min(255, Math.max(0, g / divisor));
      dst[idx + 2] = Math.min(255, Math.max(0, b / divisor));
    }
  }
  return imageData;
}

八、常见问题与解决方案

常见问题

getImageData 报安全错误怎么办?

原因是画布被跨域图片"污染"。解决方案是在加载跨域图片时设置 crossOrigin = 'anonymous',且必须在设置 src 之前设置。

像素操作太慢如何优化?

应避免频繁调用 getImageData,改为一次获取所有像素数据。可以使用 TypedArray 视图(如 Uint32Array)加速处理,或使用 Web Worker 将像素计算移到后台线程。

如何实现马赛克效果?

将图像按块大小分割,对每个块内的所有像素取平均值,然后将该块内所有像素设为该平均值。通过遍历每个块、计算 RGB 平均值并回填即可实现马赛克效果。

putImageData 为什么忽略 globalAlpha?

原因是 putImageData 直接写入像素数据,不受任何绘图状态影响。解决方案是使用临时 Canvas,先将 ImageData 绘制到临时 Canvas 上,再通过 drawImage 配合 globalAlpha 绘制到目标 Canvas。


九、总结

像素操作是 Canvas 图像处理的核心能力,关键要点:

  • getImageData:获取指定区域的像素数据

  • putImageData:将像素数据写回画布,不受变换和合成模式影响

  • createImageData:创建空白的 ImageData 对象

  • 像素格式:RGBA,每像素 4 字节,使用 Uint8ClampedArray

  • 索引计算(y * width + x) * 4 获取像素起始索引

  • 跨域限制:跨域图片会污染画布,需设置 crossOrigin

  • 性能优化:一次获取所有像素,使用 TypedArray 视图加速

下一教程将介绍 Canvas 保存与恢复状态。

标签: Canvas像素操作 getImageData putImageData 图像滤镜 像素数据处理 颜色拾取

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 多媒体:动画 - 完整教程与代码示例 下一篇: 多媒体:保存与恢复 - 完整教程与代码示例

poll相关推荐