像素级操作

5 阅读9分钟

Canvas 像素级操作 超详细讲解

Canvas 像素级操作是直接读写画布上每一个像素的颜色数据,能实现滤镜、图像合成、像素检测、粒子效果、截图处理等高级功能,是 Canvas 进阶核心能力。

我会从基础原理 → 核心API → 实战代码 → 常见应用一步步讲透,新手也能完全看懂。

一、先搞懂:Canvas 像素是什么?

Canvas 画布上的每一个点,都是一个像素(Pixel)。 每个像素由 4 个通道 组成,按固定顺序存储:

  1. R(Red):红色 0~255
  2. G(Green):绿色 0~255
  3. B(Blue):蓝色 0~255
  4. A(Alpha):透明度 0~255(0=完全透明,255=完全不透明)

关键数据结构:ImageData

像素级操作的核心对象就是 ImageData,它包含:

  • dataUint8ClampedArray 类型数组(无符号 8 位整数,值被钳在 0-255),存储所有像素的 RGBA 值
  • width:画布宽度
  • height:画布高度

像素索引计算公式(必背!)

假设画布坐标 (x, y),对应像素在 data 数组中的起始索引

const index = (y * width + x) * 4
  • y * width:找到当前行第一个像素的位置
  • + x:找到当前行第 x 列的像素
  • * 4:每个像素占 4 个位置(RGBA)

对应取值:

const r = data[index]     // 红
const g = data[index + 1] // 绿
const b = data[index + 2] // 蓝
const a = data[index + 3] // 透明度

二、像素级操作 3 个核心 API

这三个 API 是像素操作的全部基础,必须记住:

1. 获取像素数据:getImageData(x, y, w, h)

  1. ‌x‌:要提取的图像数据区域左上角的 ‌x 坐标‌。
  2. ‌y‌:要提取的图像数据区域左上角的 ‌y 坐标‌。
  3. ‌w (width)‌:要提取的矩形区域的 ‌宽度‌。
  4. ‌h (height)‌:要提取的矩形区域的 ‌高度‌。

这4个参数都是必选的

从画布指定区域读取像素,返回 ImageData 对象。

// 获取整个画布的像素
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const data = imageData.data // 像素数组

2. 创建空白像素:createImageData(w, h)

创建一个全透明的空白像素数据(所有 RGBA 都是 0)。

// 创建和画布一样大的空白像素
const blankData = ctx.createImageData(canvas.width, canvas.height)

3. 绘制像素数据:putImageData(imageData, x, y)

// 精简版(3 参数,最常用)
ctx.putImageData(imageData, dx, dy)

// 完整版(7 参数,功能更强)
ctx.putImageData(imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight)
参数详细解释
  1. imageData:像素数据对象(必须传)
  2. dx:画布上的目标 x 坐标(ImageData 画到画布的哪个位置)
  3. dy:画布上的目标 y 坐标
  4. dirtyX可选,从 ImageData 里裁剪的起始 x(默认 0)
  5. dirtyY可选,从 ImageData 里裁剪的起始 y(默认 0)
  6. dirtyWidth可选,裁剪宽度(默认整个宽度)
  7. dirtyHeight可选,裁剪高度(默认整个高度)

后 4 个参数 = 只画 ImageData 的一小块,而不是整张图

举个直观例子:
// 1. 获取整张画布像素
const imgData = ctx.getImageData(0,0,w,h)

// 只把 ImageData 里 (10,10) 位置、宽50高50的区域
// 画到画布 (0,0) 位置
ctx.putImageData(imageData, 0, 0, 10, 10, 50, 50)

//绘制整个 imageData 到画布的 (0, 0) 位置
ctx.putImageData(imgData, 0, 0);
  • dx, dy = 画到画布的哪个位置
  • dirtyX/Y/W/H = 从 ImageData 里切哪一块
  1. putImageData 官方完整参数:7 个
  2. 常用简化版:3 个imageData, dx, dy
  3. 后 4 个作用:裁剪 ImageData 局部区域绘制
  4. 核心价值:性能优化 + 局部像素更新

三、完整基础流程(万能模板)

所有像素操作都遵循这个固定步骤:

  1. 获取画布像素
  2. 遍历每一个像素
  3. 修改 RGBA 值
  4. 把像素重新绘制到画布
// 1. 获取画布和上下文
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

// 2. 获取整张画布的像素数据
const w = canvas.width
const h = canvas.height
const imageData = ctx.getImageData(0, 0, w, h)
const data = imageData.data

// 3. 遍历所有像素(核心!)
for (let y = 0; y < h; y++) {
  for (let x = 0; x < w; x++) {
    // 计算当前像素索引
    const idx = (y * w + x) * 4
    
    // 获取原颜色
    const r = data[idx]
    const g = data[idx+1]
    const b = data[idx+2]
    const a = data[idx+3]

    // ============== 在这里修改像素 ==============
    // 示例:把所有像素变成红色
    data[idx] = 255    // R
    data[idx+1] = 0    // G
    data[idx+2] = 0    // B
    data[idx+3] = 255  // A
  }
}

// 4. 把修改后的像素画回画布
ctx.putImageData(imageData, 0, 0)

四、实战:5 个最常用像素效果(直接复制可用)

1. 灰度图(黑白效果)

原理:用亮度公式计算灰度值 gray = R*0.299 + G*0.587 + B*0.114

for (let y = 0; y < h; y++) {
  for (let x = 0; x < w; x++) {
    const idx = (y * w + x) * 4
    const r = data[idx]
    const g = data[idx+1]
    const b = data[idx+2]
    
    // 计算灰度
    const gray = r * 0.299 + g * 0.587 + b * 0.114
    
    // RGB 都设为灰度值
    data[idx] = data[idx+1] = data[idx+2] = gray
  }
}

gray 通常是根据原像素的亮度计算出来的,比如用加权平均:

const gray = 0.299 * r + 0.587 * g + 0.114 * b;

这样人眼感知最自然,因为人眼对绿色最敏感,红色次之,蓝色最弱。

把 RGB 三个通道都设为同一个值 gray,这个像素就会变成灰色。原因是:

当 R=G=B 时,颜色没有色相,呈现为灰度色。

2. 反色(底片效果)

原理:每个颜色用 255 - 原值

const idx = (y * w + x) * 4
data[idx]   = 255 - data[idx]   // R
data[idx+1] = 255 - data[idx+1] // G
data[idx+2] = 255 - data[idx+2] // B

3. 黑白二值化(只有纯黑/纯白)

原理:设定阈值,大于阈值变白,小于变黑

const idx = (y * w + x) * 4
const gray = data[idx]*0.299 + data[idx+1]*0.587 + data[idx+2]*0.114
const threshold = 128 // 阈值
const val = gray > threshold ? 255 : 0
data[idx] = data[idx+1] = data[idx+2] = val

4. 透明度修改

const idx = (y * w + x) * 4
data[idx + 3] = 125 // 0-255,125=半透明

5. 像素化(马赛克效果)

原理:按块取色,让多个像素用同一个颜色

const size = 10 // 马赛克块大小
for (let y = 0; y < h; y += size) {
  for (let x = 0; x < w; x += size) {
    const idx = (y * w + x) * 4
    const r = data[idx]
    const g = data[idx+1]
    const b = data[idx+2]
    
    // 给整个块填充相同颜色
    for (let dy = 0; dy < size; dy++) {
      for (let dx = 0; dx < size; dx++) {
        const i = ((y+dy) * w + (x+dx)) * 4
        data[i] = r
        data[i+1] = g
        data[i+2] = b
      }
    }
  }
}
性能优化技巧

问题 1:dx/dy 能不能从 1 开始?跳过 (0,0) 因为已经是要填充的颜色

理论上可以,但实际不建议

  • 只节省 1 次像素写入(3 个字节赋值)
  • 却增加了每次循环的条件判断 dy === 0 ? 1 : 0
  • 条件判断的开销比省下的那次写入还大
// ❌ 不推荐 - 代码变复杂,收益几乎为零
for (let dy = 0; dy < size; dy++) {
  for (let dx = (dy === 0 ? 1 : 0); dx < size; dx++) {
    const i = ((y+dy) * w + (x+dx)) * 4
    data[i] = r; data[i+1] = g; data[i+2] = b
  }
}

结论:保持从 0 开始,代码简单,性能更好。


问题 2:先遍历 x 还是先遍历 y?有区别吗?

有区别,而且很大 —— 涉及内存访问模式。

Canvas 的 ImageData.data行优先 (row-major) 存储:

像素 0,0 | 像素 1,0 | 像素 2,0 | ... | 像素 0,1 | 像素 1,1 | ...

相邻的 x 坐标在内存中是连续的,相邻的 y 坐标相隔很远(相隔 w * 4 字节)。

// ✅ 行优先 - 缓存友好(推荐)
for (let dy = 0; dy < size; dy++) {      // 外层 y
  for (let dx = 0; dx < size; dx++) {    // 内层 x
    const i = ((y+dy) * w + (x+dx)) * 4  // 顺序访问内存
  }
}

// ❌ 列优先 - 缓存不友好(慢 2-5 倍)
for (let dx = 0; dx < size; dx++) {      // 外层 x
  for (let dy = 0; dy < size; dy++) {    // 内层 y
    const i = ((y+dy) * w + (x+dx)) * 4  // 每次跳 w*4 字节
  }
}

原因

  • CPU 缓存一次加载一整行(比如 64 字节)
  • 顺序访问时,后续数据已经在缓存里了
  • 跳跃访问时,每次都错过缓存(cache miss),要去内存读

结论:外层循环遍历 y,内层循环遍历 x(当前代码就是正确的)。

五、高级实用技巧

1. 像素拾取(获取鼠标位置的颜色)

非常常用:点击画布获取点击点的 RGB 值

canvas.addEventListener('click', (e) => {
  const x = e.offsetX
  const y = e.offsetY
  const imageData = ctx.getImageData(x, y, 1, 1)
  const [r, g, b, a] = imageData.data
  console.log('颜色:', `rgba(${r},${g},${b},${a/255})`)
})

2. 批量操作优化

遍历像素是性能敏感操作,优化技巧:

  • 缓存 width/height,不要循环内读取
  • 使用 let 代替 var
  • 大画布用分块处理

3. 注意:跨域图片限制

如果绘制了跨域图片,调用 getImageData() 会报错:

Uncaught DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.

解决方案

  1. 图片服务器配置 CORS 头
  2. 图片标签添加 crossOrigin="anonymous"
const img = new Image()
img.crossOrigin = 'anonymous' // 关键
img.src = '图片地址'

六、像素级操作能做什么?

掌握后可以实现这些高级功能:

  • 图像滤镜(复古、模糊、锐化、美颜)
  • 截图/画布导出、图片水印
  • 颜色拾取器、取色器
  • 像素艺术、画板工具
  • 图像识别预处理(人脸识别、物体检测)
  • 粒子特效、像素动画

总结

  1. 核心公式:像素索引 index = (y * width + x) * 4
  2. 核心APIgetImageData → 遍历修改 → putImageData
  3. 每个像素:固定 RGBA 4 个通道,值 0~255
  4. 万能模板:获取数据 → 双层循环遍历 → 修改通道值 → 回绘画布

只要掌握索引计算和像素遍历逻辑,你就能完全操控 Canvas 的每一个像素!

源码案例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body{
           margin: 0;
           padding: 0;

        }
        canvas {
            display: block;
        }
        /* 
            <canvas> 默认是 inline 元素,inline 元素的基线对齐(baseline alignment)会在底部预留一些空间,导致页面高度超出视口。
        */
    </style>
</head>
<body>
    <canvas id="myCanvas"></canvas>
    <script>
        const canvas = document.getElementById('myCanvas');
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        const ctx = canvas.getContext('2d');
        const image = new Image();  
        image.src = './imgs/月之暗面.png'
        // image.crossOrigin = 'anonymous';
        let w = canvas.width 
        let h = canvas.height
        image.onload = function(){ 
            ctx.drawImage(image,0,0,w,h);
            // 1.获取像素数据:getImageData(x, y, w, h)
            const imageData = ctx.getImageData(0,0,w,h);
            console.log(w,h, imageData, 'imageData-----------------');
            // 1920 * 953 * 4 = 7,319,040
            for (let y = 0; y < h; y++) {
                for (let x = 0; x < w; x++) {
                    const index = (x + y * w) * 4;
                    const red = imageData.data[index];
                    const green = imageData.data[index + 1];
                    const blue = imageData.data[index + 2];
                    const alpha = imageData.data[index + 3];
                    // console.log(red,green,blue,alpha, 'red,green,blue,alpha') //像素太多打印浏览器会卡死
                    // 案例1. 灰度图(黑白效果)
                    // 计算灰度
                    const gray = red * 0.299 + green * 0.587 + blue * 0.114
                    const data = imageData.data
                    // RGB 都设为灰度值
                    // data[index] = data[index+1] = data[index+2] = gray
                }
            }
            // 4.像素化(马赛克效果)
            let size = 20
            for (let y = 0; y < h; y += size) {
                for (let x = 0; x < w; x += size) {
                    const index = (x + y * w) * 4;
                    const red = imageData.data[index];
                    const green = imageData.data[index + 1];
                    const blue = imageData.data[index + 2];
                    const alpha = imageData.data[index + 3];
                    for (let dy = 0; dy < size; dy++) {
                        for (let dx = 0; dx < size; dx++) {
                            const index2 = (x + dx + (y + dy) * w) * 4;
                            imageData.data[index2] = red;
                            imageData.data[index2 + 1] = green;
                            imageData.data[index2 + 2] = blue;
                        }
                    }
                }
            }

            // 2.把修改后的像素画回画布:putImageData(imageData, x, y)
            ctx.putImageData(imageData, 0, 0)
            
        }
    </script>
</body>
</html>