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

时间:2018-03-08 20:56:12

标签: javascript html5 canvas

所以我在画布中使用像素操作。现在我有代码,允许你绘制到画布。然后,当你绘制了一些东西时,你可以按下一个按钮来操纵像素,将它们向右平移或向左平移一个平铺,每隔一行平移一次。代码看起来像这样:

首先,按下按钮将启动一个函数,该函数创建两个像素数据将要去的空数组。然后它逐行遍历像素,使每一行成为自己的数组。所有行数组都被添加到所有像素数据的一个数组中。

$('#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 = [];
    };

继续执行该功能,接下来它会清除画布并相互移动数组,无论是向右移动还是向左移动。

    // 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));
        };
    };

该函数的最后一部分现在获取像素数据的移位列表并将它们分发回画布。

    // 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 ++;
    }
});

截至目前,它运作良好。没有数据丢失,像素正确移位。我想知道如果可能,有没有办法做到这一点更有效?现在,逐个像素地执行此操作需要很长时间,所以我必须使用20x20像素或更高的瓷砖以获得实际的加载时间。这是我第一次尝试像素操作,所以可能有很多我不知道的事情。可能是我的笔记本电脑不够强大。此外,我注意到有时连续多次运行此功能将大大减少加载时间。任何帮助或建议都非常感谢!

功能齐全

$('#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 ++;
    }
});

2 个答案:

答案 0 :(得分:1)

性能像素操作。

给出的答案非常糟糕,我必须发布一个更好的解决方案。

在性能关键代码方面有一些建议。 功能编程在代码中没有要求尽可能最佳性能的地方。

最基本的像素操作。

该示例与其他答案相同。它使用回调来选择处理并提供一组函数来创建,过滤和设置像素数据。

因为图像可能非常大2Megp加上过滤器定时检查性能。像素数,以μs(1 / 1,000,000秒),每μs像素和每秒像素为单位的时间。对于HD 1920 * 1080的实时处理,您需要的速率为每秒125,000,000像素(60fps)。

注意 babel已关闭,以确保代码按原样运行。对不起,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>

为什么函数式编程对像素处理不利。

下面比较是使用函数式编程范例的George Campbell Answer的定时版本。速率取决于设备和浏览器,但速度要慢2个数量级。

此外,如果您单击,多次重复反转功能,您会发现GC滞后会使函数式编程成为性能代码的不良选择。

标准方法(第一个片段)没有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>

更多像素处理

下一个示例演示了一些基本的像素操作。

  • 随机。 Totaly随机像素
  • 反转。反转像素颜色
  • B / W上。转换为简单的黑白(不是感性B / W)
  • 噪声。为像素添加强噪声。会降低总亮度。
  • 2位。像素通道数据减少到每RGB 2位。
  • 模糊。大多数基本模糊功能需要像素数据的副本才能工作,因此在存储器和处理开销方面是昂贵的。但是,由于没有画布/ SVG滤镜可以使用正确的对数滤镜,因此这是获得2D画布质量模糊的唯一方法。不幸的是它很慢。
  • 频道转换。将通道蓝色移至红色,红色至绿色,绿色至蓝色
  • 随机播放像素。随机地使用其中一个邻居对像素进行随机播放。

对于较大的图像。为防止过滤器阻止页面,您可以将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[i-w] + buf[i++] * 2) / 6;
        dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
        dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + 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>

答案 1 :(得分:-1)

每个图像只使用ctx.getImageData或.createImageData之类的内容更有意义,而不是每个像素。

你可以循环ImageData.data&#34;类似数组&#34; Uint8ClampedArray。数组中的每4个项目代表一个像素,这些像素是像素的红色,绿色,蓝色和alpha部分。每个都可以是0到255之间的整数,其中[0,0,0,0,255,255,255,255,...]表示第一个像素是透明的(和黑色?),第二个像素是白色和完全不透明。

这是我刚刚制作的,没有基准但可能更有效率。

它创建图像数据,您可以通过将函数传递给编辑图像数据函数来编辑图像数据,为图像数据中的每个像素调用回调函数,并返回包含值的对象(0到255之间)和r,g,b的布尔值。

例如,对于反转,您可以返回255值。

此示例以随机像素开头,单击它们将对其应用invertRGB函数。

&#13;
&#13;
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");
});
&#13;
#canvas {
 position: absolute;
 top: 0;
 left: 0;
}
&#13;
<canvas id="canvas"></canvas>
&#13;
&#13;
&#13;

代码要点:https://gist.github.com/GCDeveloper/c02ffff1d067d6f1b1b13341a72efe79

查看https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas应该有所帮助,包括将实际图像作为ImageData加载使用。