{T}

Canvas 图像处理

本文档详细介绍 Canvas 的图像处理功能,包括图像绘制、裁剪、缩放、像素操作和滤镜效果。

图像绘制

Canvas 提供了 drawImage() 方法来绘制图像、视频或其他 Canvas。

方法签名

javascript
// 基本绘制
ctx.drawImage(image, dx, dy)

// 缩放绘制
ctx.drawImage(image, dx, dy, dWidth, dHeight)

// 裁剪绘制
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

参数说明

参数类型说明
imageHTMLImageElement/HTMLCanvasElement/HTMLVideoElement绘制源
sxNumber源图像裁剪起点 X
syNumber源图像裁剪起点 Y
sWidthNumber源图像裁剪宽度
sHeightNumber源图像裁剪高度
dxNumber目标位置 X
dyNumber目标位置 Y
dWidthNumber目标宽度
dHeightNumber目标高度

基本绘制

javascript
const img = new Image()
img.src = 'image.jpg'

img.onload = function() {
  // 在指定位置绘制原图
  ctx.drawImage(img, 50, 50)
}

缩放绘制

javascript
img.onload = function() {
  // 绘制到指定尺寸
  ctx.drawImage(img, 50, 50, 300, 200)
}

裁剪绘制

javascript
img.onload = function() {
  // 从源图像 (100, 100) 位置裁剪 200x150 区域
  // 绘制到 Canvas 的 (50, 50) 位置,尺寸为 300x225
  ctx.drawImage(
    img,
    100, 100, 200, 150,  // 源图像裁剪区域
    50, 50, 300, 225     // 目标位置和尺寸
  )
}

绘制 Canvas

javascript
const sourceCanvas = document.getElementById('sourceCanvas')
const targetCanvas = document.getElementById('targetCanvas')
const ctx = targetCanvas.getContext('2d')

// 将一个 Canvas 绘制到另一个 Canvas
ctx.drawImage(sourceCanvas, 0, 0)

绘制视频帧

html
<video id="video" src="video.mp4" style="display:none"></video>
<canvas id="canvas"></canvas>

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

  video.addEventListener('play', function() {
    function drawFrame() {
      if (video.paused || video.ended) return
      
      ctx.drawImage(video, 0, 0)
      requestAnimationFrame(drawFrame)
    }
    drawFrame()
  })
</script>

图像裁剪与缩放

图像缩放

javascript
function drawScaled(img, scale) {
  const width = img.width * scale
  const height = img.height * scale
  
  ctx.drawImage(img, 0, 0, width, height)
}

// 放大 2 倍
drawScaled(img, 2)

// 缩小到 50%
drawScaled(img, 0.5)

保持比例缩放

javascript
function drawFitToContainer(img, containerWidth, containerHeight) {
  const imgRatio = img.width / img.height
  const containerRatio = containerWidth / containerHeight
  
  let drawWidth, drawHeight, offsetX, offsetY
  
  if (imgRatio > containerRatio) {
    // 图像更宽,以宽度为准
    drawWidth = containerWidth
    drawHeight = containerWidth / imgRatio
    offsetX = 0
    offsetY = (containerHeight - drawHeight) / 2
  } else {
    // 图像更高,以高度为准
    drawHeight = containerHeight
    drawWidth = containerHeight * imgRatio
    offsetX = (containerWidth - drawWidth) / 2
    offsetY = 0
  }
  
  ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight)
}

图像裁剪

javascript
// 裁剪圆形头像
function cropCircle(img, x, y, radius) {
  ctx.save()
  
  ctx.beginPath()
  ctx.arc(x + radius, y + radius, radius, 0, Math.PI * 2)
  ctx.clip()
  
  ctx.drawImage(img, x, y, radius * 2, radius * 2)
  ctx.restore()
}

cropCircle(img, 50, 50, 60)

图像翻转

javascript
// 水平翻转
function flipHorizontal(img, x, y) {
  ctx.save()
  ctx.scale(-1, 1)
  ctx.drawImage(img, -x - img.width, y)
  ctx.restore()
}

// 垂直翻转
function flipVertical(img, x, y) {
  ctx.save()
  ctx.scale(1, -1)
  ctx.drawImage(img, x, -y - img.height)
  ctx.restore()
}

像素操作

Canvas 提供了三个方法来操作像素数据。

方法概览

方法说明
createImageData(width, height)创建空白 ImageData
getImageData(x, y, width, height)获取指定区域的像素数据
putImageData(imageData, x, y)将像素数据绘制到 Canvas

ImageData 对象

javascript
{
  width: 100,    // 宽度
  height: 100,   // 高度
  data: Uint8ClampedArray  // 像素数据数组
}

像素数据格式: [r, g, b, a, r, g, b, a, ...](每个像素 4 个值,范围 0-255)

获取像素数据

javascript
const imageData = ctx.getImageData(0, 0, 100, 100)
const data = imageData.data

console.log('宽度:', imageData.width)
console.log('高度:', imageData.height)
console.log('总像素数:', data.length / 4)

访问单个像素

javascript
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
}

遍历所有像素

javascript
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.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 a = data[i + 3]
  
  // 处理像素...
}

ctx.putImageData(imageData, 0, 0)

图像滤镜

灰度滤镜

javascript
function grayscale(imageData) {
  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 gray = r * 0.299 + g * 0.587 + b * 0.114
    
    data[i] = gray
    data[i + 1] = gray
    data[i + 2] = gray
  }
  
  return imageData
}

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
ctx.putImageData(grayscale(imageData), 0, 0)

反色滤镜

javascript
function invert(imageData) {
  const data = imageData.data
  
  for (let i = 0; i < data.length; i += 4) {
    data[i] = 255 - data[i]         // R
    data[i + 1] = 255 - data[i + 1] // G
    data[i + 2] = 255 - data[i + 2] // B
  }
  
  return imageData
}

亮度调整

javascript
function brightness(imageData, value) {
  const data = imageData.data
  
  for (let i = 0; i < data.length; i += 4) {
    data[i] += value     // R
    data[i + 1] += value // G
    data[i + 2] += value // B
  }
  
  return imageData
}

// 增加亮度
brightness(imageData, 50)

// 降低亮度
brightness(imageData, -50)

对比度调整

javascript
function contrast(imageData, value) {
  const data = imageData.data
  const factor = (259 * (value + 255)) / (255 * (259 - value))
  
  for (let i = 0; i < data.length; i += 4) {
    data[i] = factor * (data[i] - 128) + 128
    data[i + 1] = factor * (data[i + 1] - 128) + 128
    data[i + 2] = factor * (data[i + 2] - 128) + 128
  }
  
  return imageData
}

模糊效果

javascript
function blur(imageData, radius) {
  const data = imageData.data
  const width = imageData.width
  const height = imageData.height
  const copy = new Uint8ClampedArray(data)
  
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; 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 nx = x + dx
          const ny = y + dy
          
          if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
            const i = (ny * width + nx) * 4
            r += copy[i]
            g += copy[i + 1]
            b += copy[i + 2]
            count++
          }
        }
      }
      
      const i = (y * width + x) * 4
      data[i] = r / count
      data[i + 1] = g / count
      data[i + 2] = b / count
    }
  }
  
  return imageData
}

锐化效果

javascript
function sharpen(imageData) {
  const data = imageData.data
  const width = imageData.width
  const height = imageData.height
  const copy = new Uint8ClampedArray(data)
  
  // 锐化卷积核
  const kernel = [
    0, -1, 0,
    -1, 5, -1,
    0, -1, 0
  ]
  
  for (let y = 1; y < height - 1; y++) {
    for (let x = 1; x < width - 1; x++) {
      for (let c = 0; c < 3; c++) {
        let sum = 0
        let ki = 0
        
        for (let dy = -1; dy <= 1; dy++) {
          for (let dx = -1; dx <= 1; dx++) {
            const i = ((y + dy) * width + (x + dx)) * 4 + c
            sum += copy[i] * kernel[ki++]
          }
        }
        
        data[(y * width + x) * 4 + c] = sum
      }
    }
  }
  
  return imageData
}

浮雕效果

javascript
function emboss(imageData) {
  const data = imageData.data
  const width = imageData.width
  const height = imageData.height
  const copy = new Uint8ClampedArray(data)
  
  for (let y = 1; y < height - 1; y++) {
    for (let x = 1; x < width - 1; x++) {
      const i = (y * width + x) * 4
      const iPrev = ((y - 1) * width + (x - 1)) * 4
      
      for (let c = 0; c < 3; c++) {
        const val = copy[i + c] - copy[iPrev + c] + 128
        data[i + c] = val
      }
    }
  }
  
  return imageData
}

视频处理

实时视频滤镜

html
<video id="video" autoplay></video>
<canvas id="canvas"></canvas>

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

  // 获取摄像头
  navigator.mediaDevices.getUserMedia({ video: true })
    .then(stream => {
      video.srcObject = stream
    })

  video.addEventListener('play', function() {
    canvas.width = video.videoWidth
    canvas.height = video.videoHeight
    
    function processFrame() {
      ctx.drawImage(video, 0, 0)
      
      // 应用灰度滤镜
      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
      grayscale(imageData)
      ctx.putImageData(imageData, 0, 0)
      
      requestAnimationFrame(processFrame)
    }
    
    processFrame()
  })
</script>

视频截图

javascript
function captureFrame(video) {
  const canvas = document.createElement('canvas')
  canvas.width = video.videoWidth
  canvas.height = video.videoHeight
  
  const ctx = canvas.getContext('2d')
  ctx.drawImage(video, 0, 0)
  
  // 返回 base64 图像
  return canvas.toDataURL('image/png')
}

// 使用示例
const screenshot = captureFrame(video)
const img = new Image()
img.src = screenshot

综合示例

图像编辑器

javascript
class ImageEditor {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.originalImage = null
    this.currentImageData = null
  }
  
  loadImage(src) {
    return new Promise((resolve, reject) => {
      const img = new Image()
      img.crossOrigin = 'anonymous'
      img.onload = () => {
        this.originalImage = img
        this.canvas.width = img.width
        this.canvas.height = img.height
        this.ctx.drawImage(img, 0, 0)
        this.currentImageData = this.ctx.getImageData(0, 0, img.width, img.height)
        resolve()
      }
      img.onerror = reject
      img.src = src
    })
  }
  
  applyFilter(filterFn, ...args) {
    const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
    filterFn(imageData, ...args)
    this.ctx.putImageData(imageData, 0, 0)
  }
  
  reset() {
    if (this.originalImage) {
      this.ctx.drawImage(this.originalImage, 0, 0)
    }
  }
  
  save() {
    return this.canvas.toDataURL('image/png')
  }
}

// 使用示例
const editor = new ImageEditor(canvas)
await editor.loadImage('photo.jpg')
editor.applyFilter(grayscale)
editor.applyFilter(brightness, 30)
editor.applyFilter(contrast, 20)

常见问题

1. 跨域问题

问题: 图像来自不同域时,getImageData() 抛出安全错误。

解决方案:

javascript
const img = new Image()
img.crossOrigin = 'anonymous' // 需要服务器支持 CORS
img.src = 'https://example.com/image.jpg'

2. 性能优化

问题: 像素操作速度慢。

解决方案:

  • 使用 Web Worker 进行离屏处理
  • 使用 Typed Array 操作
  • 避免频繁调用 getImageDataputImageData

3. 内存泄漏

问题: 大量 ImageData 对象导致内存泄漏。

解决方案:

javascript
// 及时释放引用
imageData = null

// 使用对象池复用 ImageData
const imagePool = []

function getImageData(width, height) {
  if (imagePool.length > 0) {
    return imagePool.pop()
  }
  return ctx.createImageData(width, height)
}

function releaseImageData(imageData) {
  imagePool.push(imageData)
}

下一步学习


返回Canvas 教程目录 | 上一篇文本绘制