阿里云主机折上折
  • 微信号
Current Site:Index > Canvas image processing (pixel manipulation, filters, etc.)

Canvas image processing (pixel manipulation, filters, etc.)

Author:Chuan Chen 阅读数:57791人阅读 分类: HTML

Canvas Image Processing Basics

HTML5 Canvas provides powerful image processing capabilities, allowing direct manipulation of pixel data to achieve various effects. The getImageData() and putImageData() methods of the CanvasRenderingContext2D interface enable developers to read and modify pixel data on the canvas. Each pixel consists of four RGBA components, each ranging from 0 to 255.

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;

// Iterate through all pixels
for (let i = 0; i < data.length; i += 4) {
    // data[i] = red
    // data[i+1] = green
    // data[i+2] = blue
    // data[i+3] = alpha
}

Basic Pixel Operations

Grayscale Conversion

Converting a color image to grayscale is a fundamental image processing operation. Common algorithms include the average method and weighted average method.

function grayscale(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;
}

Invert Effect

The invert effect is achieved by inverting each color component.

function invert(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;
}

Advanced Filter Implementation

Convolution Filters

Convolution is a common technique in image processing, achieved by applying a kernel matrix to image pixels.

function applyConvolution(imageData, kernel, divisor = 1, offset = 0) {
    const width = imageData.width;
    const height = imageData.height;
    const srcData = new Uint8ClampedArray(imageData.data);
    const dstData = imageData.data;
    
    const kRows = kernel.length;
    const kCols = kernel[0].length;
    const halfKRows = Math.floor(kRows / 2);
    const halfKCols = Math.floor(kCols / 2);
    
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            let r = 0, g = 0, b = 0;
            
            for (let ky = 0; ky < kRows; ky++) {
                for (let kx = 0; kx < kCols; kx++) {
                    const px = Math.min(width - 1, Math.max(0, x + kx - halfKCols));
                    const py = Math.min(height - 1, Math.max(0, y + ky - halfKRows));
                    const pos = (py * width + px) * 4;
                    
                    const weight = kernel[ky][kx];
                    r += srcData[pos] * weight;
                    g += srcData[pos + 1] * weight;
                    b += srcData[pos + 2] * weight;
                }
            }
            
            const pos = (y * width + x) * 4;
            dstData[pos] = (r / divisor + offset) | 0;
            dstData[pos + 1] = (g / divisor + offset) | 0;
            dstData[pos + 2] = (b / divisor + offset) | 0;
        }
    }
    
    return imageData;
}

Common Kernel Examples

Blur Effect

const blurKernel = [
    [1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]
];
applyConvolution(imageData, blurKernel, 9);

Sharpen Effect

const sharpenKernel = [
    [0, -1, 0],
    [-1, 5, -1],
    [0, -1, 0]
];
applyConvolution(imageData, sharpenKernel);

Edge Detection

const edgeDetectKernel = [
    [-1, -1, -1],
    [-1, 8, -1],
    [-1, -1, -1]
];
applyConvolution(imageData, edgeDetectKernel);

Color Adjustments

Brightness Adjustment

function adjustBrightness(imageData, value) {
    const data = imageData.data;
    for (let i = 0; i < data.length; i += 4) {
        data[i] += value;     // red
        data[i + 1] += value; // green
        data[i + 2] += value; // blue
    }
    return imageData;
}

Contrast Adjustment

function adjustContrast(imageData, contrast) {
    const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
    const data = imageData.data;
    
    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;
}

Hue/Saturation Adjustment

function adjustHSB(imageData, hueDelta, saturationFactor, brightnessFactor) {
    const data = imageData.data;
    
    for (let i = 0; i < data.length; i += 4) {
        let r = data[i] / 255;
        let g = data[i + 1] / 255;
        let b = data[i + 2] / 255;
        
        // RGB to HSV
        const max = Math.max(r, g, b);
        const min = Math.min(r, g, b);
        let h, s, v = max;
        
        const d = max - min;
        s = max === 0 ? 0 : d / max;
        
        if (max === min) {
            h = 0;
        } else {
            switch (max) {
                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                case g: h = (b - r) / d + 2; break;
                case b: h = (r - g) / d + 4; break;
            }
            h /= 6;
        }
        
        // Adjust HSV
        h = (h + hueDelta / 360) % 1;
        if (h < 0) h += 1;
        s = Math.min(1, Math.max(0, s * saturationFactor));
        v = Math.min(1, Math.max(0, v * brightnessFactor));
        
        // HSV to RGB
        const i = Math.floor(h * 6);
        const f = h * 6 - i;
        const p = v * (1 - s);
        const q = v * (1 - f * s);
        const t = v * (1 - (1 - f) * s);
        
        let nr, ng, nb;
        switch (i % 6) {
            case 0: nr = v; ng = t; nb = p; break;
            case 1: nr = q; ng = v; nb = p; break;
            case 2: nr = p; ng = v; nb = t; break;
            case 3: nr = p; ng = q; nb = v; break;
            case 4: nr = t; ng = p; nb = v; break;
            case 5: nr = v; ng = p; nb = q; break;
        }
        
        data[i] = nr * 255;
        data[i + 1] = ng * 255;
        data[i + 2] = nb * 255;
    }
    
    return imageData;
}

Performance Optimization Techniques

Using Web Workers

For large image processing tasks, use Web Workers to avoid blocking the UI thread.

// Main thread
const worker = new Worker('image-worker.js');
worker.postMessage({imageData, operation: 'grayscale'});
worker.onmessage = function(e) {
    ctx.putImageData(e.data, 0, 0);
};

// image-worker.js
self.onmessage = function(e) {
    const imageData = e.data.imageData;
    // Process image
    self.postMessage(processedImageData, [processedImageData.data.buffer]);
};

Using TypedArray Optimization

Directly manipulating Uint8ClampedArray is faster than using the ImageData.data interface.

function fastGrayscale(imageData) {
    const data = new Uint8Array(imageData.data.buffer);
    for (let i = 0; i < data.length; i += 4) {
        const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
        data[i] = data[i + 1] = data[i + 2] = avg;
    }
    return imageData;
}

Chunk Processing

For very large images, process in chunks to avoid memory issues.

function processInChunks(ctx, width, height, processFn, chunkSize = 256) {
    for (let y = 0; y < height; y += chunkSize) {
        for (let x = 0; x < width; x += chunkSize) {
            const w = Math.min(chunkSize, width - x);
            const h = Math.min(chunkSize, height - y);
            const imageData = ctx.getImageData(x, y, w, h);
            const processed = processFn(imageData);
            ctx.putImageData(processed, x, y);
        }
    }
}

Advanced Image Processing Techniques

Histogram-Based Processing

Histogram Equalization

function histogramEqualization(imageData) {
    const data = imageData.data;
    const hist = new Array(256).fill(0);
    const width = imageData.width;
    const height = imageData.height;
    const total = width * height;
    
    // Calculate histogram
    for (let i = 0; i < data.length; i += 4) {
        const gray = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
        hist[gray]++;
    }
    
    // Calculate cumulative distribution
    let sum = 0;
    const cdf = hist.map(count => {
        sum += count;
        return sum;
    });
    
    // Normalize
    const cdfMin = Math.min(...cdf.filter(v => v > 0));
    const scale = 255 / (total - cdfMin);
    
    // Apply transformation
    for (let i = 0; i < data.length; i += 4) {
        const gray = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
        const equalized = Math.round((cdf[gray] - cdfMin) * scale);
        data[i] = data[i + 1] = data[i + 2] = equalized;
    }
    
    return imageData;
}

Threshold-Based Binarization

function threshold(imageData, thresholdValue) {
    const data = imageData.data;
    for (let i = 0; i < data.length; i += 4) {
        const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
        const value = gray > thresholdValue ? 255 : 0;
        data[i] = data[i + 1] = data[i + 2] = value;
    }
    return imageData;
}

Adaptive Threshold

function adaptiveThreshold(imageData, blockSize = 15, C = 5) {
    const width = imageData.width;
    const height = imageData.height;
    const integral = new Array(width * height).fill(0);
    const data = new Uint8Array(imageData.data.buffer);
    
    // Calculate integral image
    for (let y = 0; y < height; y++) {
        let sum = 0;
        for (let x = 0; x < width; x++) {
            const pos = y * width + x;
            const gray = Math.round(0.299 * data[pos * 4] + 0.587 * data[pos * 4 + 1] + 0.114 * data[pos * 4 + 2]);
            sum += gray;
            integral[pos] = (y > 0 ? integral[pos - width] : 0) + sum;
        }
    }
    
    // Apply adaptive threshold
    const halfBlock = Math.floor(blockSize / 2);
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const pos = y * width + x;
            const gray = Math.round(0.299 * data[pos * 4] + 0.587 * data[pos * 4 + 1] + 0.114 * data[pos * 4 + 2]);
            
            // Calculate region boundaries
            const x1 = Math.max(0, x - halfBlock);
            const y1 = Math.max(0, y - halfBlock);
            const x2 = Math.min(width - 1, x + halfBlock);
            const y2 = Math.min(height - 1, y + halfBlock);
            
            // Calculate region area
            const area = (x2 - x1 + 1) * (y2 - y1 + 1);
            
            // Use integral image to quickly calculate region sum
            const a = y1 > 0 && x1 > 0 ? integral[(y1 - 1) * width + (x1 - 1)] : 0;
            const b = y1 > 0 ? integral[(y1 - 1) * width + x2] : 0;
            const c = x1 > 0 ? integral[y2 * width + (x1 - 1)] : 0;
            const d = integral[y2 * width + x2];
            
            const sum = d - b - c + a;
            const mean = sum / area;
            
            data[pos * 4] = data[pos * 4 + 1] = data[pos * 4 + 2] = gray > (mean - C) ? 255 : 0;
        }
    }
    
    return imageData;
}

Blend Modes and Compositing

Canvas supports various blend modes through the globalCompositeOperation property.

// Normal mode
ctx.globalCompositeOperation = 'source-over';

// Common blend mode examples
const blendModes = [
    'multiply',    // Multiply
    'screen',      // Screen
    'overlay',     // Overlay
    'darken',      // Darken
    'lighten',     // Lighten
    'color-dodge', // Color Dodge
    'color-burn',  // Color Burn
    'hard-light',  // Hard Light
    'soft-light',  // Soft Light
    'difference',  // Difference
    'exclusion',   // Exclusion
    'hue',         // Hue
    'saturation',  // Saturation
    'color',       // Color
    'luminosity'   // Luminosity
];

// Apply blend mode
function applyBlendMode(ctx, imageData, mode) {
    ctx.putImageData(imageData, 0, 0);
    ctx.globalCompositeOperation = mode;
    ctx.fillStyle = 'rgba(200, 150, 100, 0.5)';
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}

Image Warping and Distortion

Ripple Effect

function rippleEffect(imageData, amplitude, frequency) {
    const width = imageData.width;
    const height = imageData.height;
    const srcData = new Uint8ClampedArray(imageData.data);
    const dstData = imageData.data;
    
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            // Calculate ripple offset
            const offsetX = Math.round(amplitude * Math.sin(2 * Math.PI * y / frequency));
            const offsetY = Math.round(amplitude * Math.cos(2 * Math.PI * x / frequency));
            
            // Boundary check
            const srcX = Math.max(0, Math.min(width - 1, x + offsetX));
            const srcY = Math.max(0, Math.min(height - 1, y + offsetY));
            
            // Copy pixel
            const srcPos = (srcY * width + srcX) * 4;
            const dstPos = (y * width + x) * 4;
            
            dstData[dstPos] = srcData[srcPos];
            dstData[dstPos + 1] = srcData[srcPos + 1];
            dstData[dstPos + 2] = srcData[srcPos + 2];
            dstData[dstPos + 3] = srcData[srcPos + 3];
        }
    }
    
    return imageData;
}

Spherical Distortion

function sphereDistortion(imageData, radius, centerX, centerY) {
    const width = imageData.width;
    const height = imageData.height;
    const srcData = new Uint8ClampedArray(imageData.data);
    const dstData = imageData.data;

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.