pin_drop当前位置:知识文库 ❯ 图文
多媒体:像素操作 - 完整教程与代码示例
一、教程简介
Canvas 像素操作允许开发者直接读写画布上的每一个像素数据,这是实现图像滤镜、颜色调整、图像合成等高级效果的基础。通过 getImageData 获取像素数据,通过 putImageData 写回像素数据,配合 createImageData 创建空白图像数据,可以实现各种图像处理效果。本教程将详细讲解像素操作的原理、方法和实际应用。
二、核心概念
ImageData 对象
ImageData 对象包含三个属性:
像素数据格式
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);putImageData - 写回像素数据
代码示例
ctx.putImageData(imageData, dx, dy);
ctx.putImageData(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>五、浏览器兼容性
注意:跨域图片会导致
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 保存与恢复状态。
本文涉及AI创作
内容由AI创作,请仔细甄别