简体   繁体   English

如何有效地操纵HTML5画布中的像素?

[英]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. 现在,逐个像素执行此操作需要花费很长时间,因此我必须经过20x20 px或更高的瓦片才能获得实际的加载时间。 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. 因为图像可能非常大,所以2Megp加上了过滤器的时间,以检查性能。 The number of pixels, time taken in µs (1/1,000,000th second), pixels per µs and pixels per second. 像素数,以微秒(1 / 1,000,000秒)为单位的时间,每微秒的像素和每秒的像素。 For realtime processing of a HD 1920*1080 you need a rate of ~125,000,000 pixels per second (60fps). 对于高清1920 * 1080的实时处理,您需要每秒约125,000,000像素(60fps)的速率。

NOTE babel has been turned off to ensure code is run as is. 注意 babel已关闭,以确保代码按原样运行。 Sorry IE11 users time to upgrade don`t you think? 对不起IE11用户时间升级,您不觉得吗?

 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. 下面进行比较的是使用功能编程范例的George Campbell Answer的定时版本。 The rate will depend on the device and browser but is 2 orders of magnitude slower. 速率将取决于设备和浏览器,但要慢2个数量级。

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. 同样,如果单击多次重复反转函数,您会发现GC滞后使函数式编程成为性能代码的错误选择。

The standard method (first snippet) does not suffer from GC lag because it barely uses any memory apart from the original pixel buffer. 标准方法(第一个代码段)不受GC延迟的影响,因为它几乎不使用除原始像素缓冲区之外的任何内存。

 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. 2位。 Pixel channel data is reduced to 2 bits per RGB. 像素通道数据减少到每个RGB 2位。
  • 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. 但是,由于没有一个画布/ SVG滤镜可以执行正确的对数滤镜,所以这是为2D画布获得高质量模糊效果的唯一方法。 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. 为了防止过滤器阻塞页面,您可以将imageData移动到工作程序并在其中处理像素。

 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. 每个图像只使用一次ctx.getImageData或.createImageData之类的东西,而不是每个像素,才有意义。

You can loop the ImageData.data "array-like" Uint8ClampedArray. 您可以循环ImageData.data“类似数组”的Uint8ClampedArray。 Each 4 items in the array represent a single pixel, these being red, green, blue, and alpha parts of the pixel. 阵列中的每4个项目代表一个像素,它们是像素的红色,绿色,蓝色和alpha部分。 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. 每个像素可以是0到255之间的整数,其中[0,0,0,0,0,255,255,255,255,...]表示第一个像素是透明的(和黑色?),第二个像素是白色且完全不透明。

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. 它创建图像数据,您可以通过将一个函数传递给Edit Image Data函数来编辑图像数据,对图像数据中的每个像素调用回调函数,并返回一个包含值(0到255之间)和boolean值的对象对于r,g,b。

For example for invert you can return 255-value. 例如对于反转,您可以返回255值。

this example starts with random pixels, clicking them will apply the invertRGB function to it. 此示例以随机像素开始,单击它们将对其应用invertRGB功能。

 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 代码要点: 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. 请查看https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas ,这应该会有所帮助,包括将实际图像加载为ImageData以供使用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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