简体   繁体   中英

How to efficiently manipulate pixels in HTML5 canvas?

So I am fooling around with pixel manipulation in canvas. Right now I have code that allows you to draw to canvas. Then, when you have something drawn, there is a button you can press to manipulate the pixels, translating them either one tile to the right or one tile to the left, alternating every other row. The code looks something like this:

First, pushing the button will start a function that creates two empty arrays where the pixel data is going to go. Then it goes through the pixels, row by row, making each row it's own array. All the row arrays are added into one array of all the pixels data.

$('#shift').click(function() {

    var pixels = [];
    var rowArray = [];

    // get a list of all pixels in a row and add them to pixels array
    for (var y = 0; y < canvas.height; y ++) {
        for (var x = 0; x < canvas.width; x ++) {
            var src = ctx.getImageData(x, y, 1, 1)
            var copy = ctx.createImageData(src.width, src.height);
            copy.data.set(src.data);

            pixels.push(copy);
        };
        rowArray.push(pixels);
        pixels = [];
    };

Continuing in the function, next it clears the canvas and shifts the arrays every other either going one to the right or one to the left.

    // clear canvas and points list
    clearCanvas(ctx);

    // take copied pixel lists, shift them
    for (i = 0; i < rowArray.length; i ++) {
        if (i % 2 == 0) {
            rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, 1));
        } else {
            rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, rowArray[i].length - 1));
        };
    };

Last part of the function now takes the shifted lists of pixel data and distributes them back onto the canvas.

    // take the new shifted pixel lists and distribute
    // them back onto the canvas
    var listCounter = 0;
    var listCounter2 = 0;
    for (var y = 0; y < canvas.height; y ++) {
        for (var x = 0; x < canvas.width; x ++) {   
            ctx.putImageData(rowArray[listCounter][listCounter2], x, y);

            listCounter2 ++;    
        }

        listCounter2 = 0;
        listCounter ++;
    }
});

As of right now, it works fine. No data is lost and pixels are shifted correctly. What I am wondering if possible, is there a way to do this that is more efficient? Right now, doing this pixel by pixel takes a long time so I have to go by 20x20 px tiles or higher to have realistic load times. This is my first attempt at pixel manipulation so there is probably quite a few things I'm unaware of. It could be my laptop is not powerful enough. Also, I've noticed that sometimes running this function multiple times in a row will significantly reduce load times. Any help or suggestions are much appreciated!

Full function :

$('#shift').click(function() {

    var pixels = [];
    var rowArray = [];

    // get a list of all pixels in a row and add them to pixels array
    for (var y = 0; y < canvas.height; y ++) {
        for (var x = 0; x < canvas.width; x ++) {
            var src = ctx.getImageData(x, y, 1, 1)
            var copy = ctx.createImageData(src.width, src.height);
            copy.data.set(src.data);

            pixels.push(copy);
        };
        rowArray.push(pixel);
        pixels = [];
    };

    // clear canvas and points list
    clearCanvas(ctx);

    // take copied pixel lists, shift them
    for (i = 0; i < pixelsListList.length; i ++) {
        if (i % 2 == 0) {
            rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, 1));
        } else {
            rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, rowArray[i].length - 1));
        };
    };

    // take the new shifted pixel lists and distribute
    // them back onto the canvas
    var listCounter = 0;
    var listCounter2 = 0;
    for (var y = 0; y < canvas.height; y ++) {
        for (var x = 0; x < canvas.width; x ++) {   
            ctx.putImageData(rowArray[listCounter][listCounter2], x, y);

            listCounter2 ++;    
        }

        listCounter2 = 0;
        listCounter ++;
    }
});

Performance pixel manipulation.

The given answer is so bad that I have to post a better solution.

And with that a bit of advice when it comes to performance critical code. Functional programming has no place in code that requires the best performance possible.

The most basic pixel manipulation.

The example does the same as the other answer. It uses a callback to select the processing and provides a set of functions to create, filter, and set the pixel data.

Because images can be very large 2Megp plus the filter is timed to check performance. The number of pixels, time taken in µs (1/1,000,000th second), pixels per µs and pixels per second. For realtime processing of a HD 1920*1080 you need a rate of ~125,000,000 pixels per second (60fps).

NOTE babel has been turned off to ensure code is run as is. Sorry IE11 users time to upgrade don`t you think?

 canvas.addEventListener('click', ()=>{ var time = performance.now(); ctx.putImageData(processPixels(randomPixels,invertPixels), 0, 0); time = (performance.now() - time) * 1000; var rate = pixelCount / time; var pps = (1000000 * rate | 0).toLocaleString(); info.textContent = "Time to process " + pixelCount.toLocaleString() + " pixels : " + (time | 0).toLocaleString() + "µs, "+ (rate|0) + "pix per µs "+pps+" pixel per second"; }); const ctx = canvas.getContext("2d"); const pixelCount = innerWidth * innerHeight; canvas.width = innerWidth; canvas.height = innerHeight; const randomPixels = putPixels(ctx,createImageData(canvas.width, canvas.height, randomRGB)); function createImageData(width, height, filter){ return processPixels(ctx.createImageData(width, height), filter);; } function processPixels(pixelData, filter = doNothing){ return filter(pixelData); } function putPixels(context,pixelData,x = 0,y = 0){ context.putImageData(pixelData,x,y); return pixelData; } // Filters must return pixeldata function doNothing(pd){ return pd } function randomRGB(pixelData) { var i = 0; var dat32 = new Uint32Array(pixelData.data.buffer); while (i < dat32.length) { dat32[i++] = 0xff000000 + Math.random() * 0xFFFFFF } return pixelData; } function invertPixels(pixelData) { var i = 0; var dat = pixelData.data; while (i < dat.length) { dat[i] = 255 - dat[i++]; dat[i] = 255 - dat[i++]; dat[i] = 255 - dat[i++]; i ++; // skip alpha } return pixelData; } 
 .abs { position: absolute; top: 0px; left: 0px; font-family : arial; font-size : 16px; background : rgba(255,255,255,0.75); } .m { top : 100px; z-index : 10; } #info { z-index : 10; } 
 <div class="abs" id="info"></div> <div class="abs m">Click to invert</div> <canvas class="abs" id="canvas"></canvas> 

Why functional programming is bad for pixel processing.

To compare below is a timed version of George Campbell Answer that uses functional programming paradigms. The rate will depend on the device and browser but is 2 orders of magnitude slower.

Also if you click, repeating the invert function many times you will notice the GC lags that make functional programming such a bad choice for performance code.

The standard method (first snippet) does not suffer from GC lag because it barely uses any memory apart from the original pixel buffer.

 let canvas = document.getElementById("canvas"); let ctx = canvas.getContext("2d"); //maybe put inside resize event listener let width = window.innerWidth; let height = window.innerHeight; canvas.width = width; canvas.height = height; const pixelCount = innerWidth * innerHeight; //create some test pixels (random colours) - only once for entire width/height, not for each pixel let randomPixels = createImageData(width, height, randomRGB); //create image data and apply callback for each pixel, set this in the ImageData function createImageData(width, height, cb){ let createdPixels = ctx.createImageData(width, height); if(cb){ let pixelData = editImageData(createdPixels, cb); createdPixels.data.set(pixelData); } return createdPixels; } //edit each pixel in ImageData using callback //pixels ImageData, cb Function (for each pixel, returns r,g,b,a Boolean) function editImageData(pixels, cb = (p)=>p){ return Array.from(pixels.data).map((pixel, i) => { //red or green or blue or alpha let newValue = cb({r: i%4 === 0, g:i%4 === 1, b:i%4 === 2, a:i%4 === 3, value: pixel}); if(typeof newValue === 'undefined' || newValue === null){ throw new Error("undefined/null pixel value "+typeof newValue+" "+newValue); } return newValue; }); } //callback to apply to each pixel (randomize) function randomRGB({a}){ if(a){ return 255; //full opacity } return Math.floor(Math.random()*256); }; //another callback to apply, this time invert function invertRGB({a, value}){ if(a){ return 255; //full opacity } return 255-value; }; ctx.putImageData(randomPixels, 0, 0); //click to change invert image data (or any custom pixel manipulation) canvas.addEventListener('click', ()=>{ var time = performance.now(); randomPixels.data.set(editImageData(randomPixels, invertRGB)); ctx.putImageData(randomPixels, 0, 0); time = (performance.now() - time) * 1000; var rate = pixelCount / time; var pps = (1000000 * rate | 0).toLocaleString(); if(rate < 1){ rate = "less than 1"; } info.textContent = "Time to process " + pixelCount.toLocaleString() + " pixels : " + (time|0).toLocaleString() + "µs, "+ rate + "pix per µs "+pps+" pixel per second"; }); 
 .abs { position: absolute; top: 0px; left: 0px; font-family : arial; font-size : 16px; background : rgba(255,255,255,0.75); } .m { top : 100px; z-index : 10; } #info { z-index : 10; } 
 <div class="abs" id="info"></div> <div class="abs m">George Campbell Answer. Click to invert</div> <canvas class="abs" id="canvas"></canvas> 

Some more pixel processing

The next sample demonstrates some basic pixel manipulation.

  • Random. Totaly random pixels
  • Invert. Inverts the pixel colors
  • B/W. Converts to simple black and white (not perceptual B/W)
  • Noise. Adds strong noise to pixels. Will reduce total brightness.
  • 2 Bit. Pixel channel data is reduced to 2 bits per RGB.
  • Blur. Most basic blur function requires a copy of the pixel data to work and is thus expensive in terms of memory and processing overheads. But as NONE of the canvas/SVG filters do the correct logarithmic filter this is the only way to get a good quality blur for the 2D canvas. Unfortunately it is rather slow.
  • Channel Shift. Moves channels blue to red, red to green, green to blue
  • Shuffle pixels. Randomly shuffles pixels with one of its neighbours.

For larger images. To prevent filters from blocking the page you would move the imageData to a worker and process the pixels there.

 document.body.addEventListener('click', (e)=>{ if(e.target.type !== "button" || e.target.dataset.filter === "test"){ testPattern(); pixels = getImageData(ctx); info.textContent = "Untimed content render." return; } var time = performance.now(); ctx.putImageData(processPixels(pixels,pixelFilters[e.target.dataset.filter]), 0, 0); time = (performance.now() - time) * 1000; var rate = pixelCount / time; var pps = (1000000 * rate | 0).toLocaleString(); info.textContent = "Filter "+e.target.value+ " " +(e.target.dataset.note ? e.target.dataset.note : "") + pixelCount.toLocaleString() + "px : " + (time | 0).toLocaleString() + "µs, "+ (rate|0) + "px per µs "+pps+" pps"; }); const ctx = canvas.getContext("2d"); const pixelCount = innerWidth * innerHeight; canvas.width = innerWidth; canvas.height = innerHeight; var min = Math.min(innerWidth,innerHeight) * 0.45; function testPattern(){ var grad = ctx.createLinearGradient(0,0,0,canvas.height); grad.addColorStop(0,"#000"); grad.addColorStop(0.5,"#FFF"); grad.addColorStop(1,"#000"); ctx.fillStyle = grad; ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height); "000,AAA,FFF,F00,00F,A00,00A,FF0,0FF,AA0,0AA,0F0,F0F,0A0,A0A".split(",").forEach((col,i) => { circle("#"+col, min * (1-i/16)); }); } function circle(col,size){ ctx.fillStyle = col; ctx.beginPath(); ctx.arc(canvas.width / 2, canvas.height / 2, size, 0 , Math.PI * 2); ctx.fill(); } testPattern(); var pixels = getImageData(ctx); function getImageData(ctx, x = 0, y = 0,width = ctx.canvas.width, height = ctx.canvas.height){ return ctx.getImageData(x,y,width, height); } function createImageData(width, height, filter){ return processPixels(ctx.createImageData(width, height), filter);; } function processPixels(pixelData, filter = doNothing){ return filter(pixelData); } function putPixels(context,pixelData,x = 0,y = 0){ context.putImageData(pixelData,x,y); return pixelData; } // Filters must return pixeldata function doNothing(pd){ return pd } function randomRGB(pixelData) { var i = 0; var dat32 = new Uint32Array(pixelData.data.buffer); while (i < dat32.length) { dat32[i++] = 0xff000000 + Math.random() * 0xFFFFFF } return pixelData; } function randomNoise(pixelData) { var i = 0; var dat = pixelData.data; while (i < dat.length) { dat[i] = Math.random() * dat[i++]; dat[i] = Math.random() * dat[i++]; dat[i] = Math.random() * dat[i++]; i ++; // skip alpha } return pixelData; } function twoBit(pixelData) { var i = 0; var dat = pixelData.data; var scale = 255 / 196; while (i < dat.length) { dat[i] = (dat[i++] & 196) * scale; dat[i] = (dat[i++] & 196) * scale; dat[i] = (dat[i++] & 196) * scale; i ++; // skip alpha } return pixelData; } function invertPixels(pixelData) { var i = 0; var dat = pixelData.data; while (i < dat.length) { dat[i] = 255 - dat[i++]; dat[i] = 255 - dat[i++]; dat[i] = 255 - dat[i++]; i ++; // skip alpha } return pixelData; } function simpleBW(pixelData) { var bw,i = 0; var dat = pixelData.data; while (i < dat.length) { bw = (dat[i] + dat[i+1] + dat[i+2]) / 3; dat[i++] = bw; dat[i++] = bw; dat[i++] = bw; i ++; // skip alpha } return pixelData; } function simpleBlur(pixelData) { var i = 0; var dat = pixelData.data; var buf = new Uint8Array(dat.length); buf.set(dat); var w = pixelData.width * 4; i += w; while (i < dat.length - w) { dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[iw] + buf[i++] * 2) / 6; dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[iw] + buf[i++] * 2) / 6; dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[iw] + buf[i++] * 2) / 6; i ++; // skip alpha } return pixelData; } function channelShift(pixelData) { var r,g,i = 0; var dat = pixelData.data; while (i < dat.length) { r = dat[i]; g = dat[i+1]; dat[i] = dat[i+2]; dat[i+1] = r; dat[i+2] = g; i += 4; } return pixelData; } function pixelShuffle(pixelData) { var r,g,b,n,rr,gg,bb,i = 0; var dat = pixelData.data; var next = [-pixelData.width*4,pixelData.width*4,-4,4]; var len = dat.length; while (i < dat.length) { n = (i + next[Math.random() * 4 | 0]) % len; r = dat[i]; g = dat[i+1]; b = dat[i+2]; dat[i] = dat[n]; dat[i+1] = dat[n + 1]; dat[i+2] = dat[n + 2]; dat[n] = r; dat[n+1] = g; dat[n+2] = b; i += 4; } return pixelData; } const pixelFilters = { randomRGB, invertPixels, simpleBW, randomNoise, twoBit, simpleBlur, channelShift, pixelShuffle, } 
 .abs { position: absolute; top: 0px; left: 0px; font-family : arial; font-size : 16px; } .m { top : 30px; z-index : 20; } #info { z-index : 10; background : rgba(255,255,255,0.75); } 
 <canvas class="abs" id="canvas"></canvas> <div class="abs" id="buttons"> <input type ="button" data-filter = "randomRGB" value ="Random"/> <input type ="button" data-filter = "invertPixels" value ="Invert"/> <input type ="button" data-filter = "simpleBW" value ="B/W"/> <input type ="button" data-filter = "randomNoise" value ="Noise"/> <input type ="button" data-filter = "twoBit" value ="2 Bit" title = "pixel channel data is reduced to 2 bits per RGB"/> <input type ="button" data-note="High quality blur using logarithmic channel values. " data-filter = "simpleBlur" value ="Blur" title = "Blur requires a copy of pixel data"/> <input type ="button" data-filter = "channelShift" value ="Ch Shift" title = "Moves channels blue to red, red to green, green to blue"/> <input type ="button" data-filter = "pixelShuffle" value ="Shuffle" title = "randomly shuffles pixels with one of its neighbours"/> <input type ="button" data-filter = "test" value ="Test Pattern"/> </div> <div class="abs m" id="info"></div> 

It makes more sense to use something like ctx.getImageData or .createImageData only once per image, not for each pixel.

You can loop the ImageData.data "array-like" Uint8ClampedArray. Each 4 items in the array represent a single pixel, these being red, green, blue, and alpha parts of the pixel. Each can be an integer between 0 and 255, where [0,0,0,0,255,255,255,255,...] means the first pixel is transparent (and black?), and the second pixel is white and full opacity.

here is something I just made, not benchmarked but likely more efficient.

It creates image data, and you can edit image data by passing in a function to the edit image data function, the callback function is called for each pixel in an image data and returns an object containing value (between 0 and 255), and booleans for r, g, b.

For example for invert you can return 255-value.

this example starts with random pixels, clicking them will apply the invertRGB function to it.

 let canvas = document.getElementById("canvas"); let ctx = canvas.getContext("2d"); //maybe put inside resize event listener let width = window.innerWidth; let height = window.innerHeight; canvas.width = width; canvas.height = height; //create some test pixels (random colours) - only once for entire width/height, not for each pixel let randomPixels = createImageData(width, height, randomRGB); //create image data and apply callback for each pixel, set this in the ImageData function createImageData(width, height, cb){ let createdPixels = ctx.createImageData(width, height); if(cb){ let pixelData = editImageData(createdPixels, cb); createdPixels.data.set(pixelData); } return createdPixels; } //edit each pixel in ImageData using callback //pixels ImageData, cb Function (for each pixel, returns r,g,b,a Boolean) function editImageData(pixels, cb = (p)=>p){ let i = 0; let len = pixels.data.length; let outputPixels = []; for(i=0;i<len;i++){ let pixel = pixels.data[i]; outputPixels.push( cb(i%4, pixel) ); } return outputPixels; } //callback to apply to each pixel (randomize) function randomRGB(colour){ if( colour === 3){ return 255; //full opacity } return Math.floor(Math.random()*256); }; //another callback to apply, this time invert function invertRGB(colour, value){ if(colour === 3){ return 255; //full opacity } return 255-value; }; ctx.putImageData(randomPixels, 0, 0); //click to change invert image data (or any custom pixel manipulation) canvas.addEventListener('click', ()=>{ let t0 = performance.now(); randomPixels.data.set(editImageData(randomPixels, invertRGB)); ctx.putImageData(randomPixels, 0, 0); let t1 = performance.now(); console.log(t1-t0+"ms"); }); 
 #canvas { position: absolute; top: 0; left: 0; } 
 <canvas id="canvas"></canvas> 

code gist: https://gist.github.com/GCDeveloper/c02ffff1d067d6f1b1b13341a72efe79

check out https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas which should help, including loading an actual image as ImageData for usage.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM