像素化图像数据算法非常慢

时间:2016-04-17 15:15:59

标签: javascript algorithm canvas

我的工具中有这个像素化算法,但是当我今天将它应用到我的绘图应用程序时,它的性能非常糟糕。我想知道你是否可以帮助我。

这是我的算法:

//apply pixalate algorithm
for(var x = 1; x < w; x += aOptions.blockSize)
{
    for(var y = 1; y < h; y += aOptions.blockSize)
    {
        var pixel = sctx.getImageData(x, y, 1, 1);
        dctx.fillStyle = "rgb("+pixel.data[0]+","+pixel.data[1]+","+pixel.data[2]+")";
        dctx.fillRect(x, y, x + aOptions.blockSize - 1, y + aOptions.blockSize - 1);
    }
}

我想知道你是否可以帮助我加快速度,我不确定是什么造成这种性能上升。

(是的,我知道imageSmoothingEnabled techqnique,但它不能让我完全控制块大小)

2 个答案:

答案 0 :(得分:4)

快速硬件像素化

GPU可以做到这一点..

为什么不使用GPU来执行drawImage调用,它更快。在像素化中以所需像素数绘制较小的源画布,然后将其绘制到缩放后的源。您只需要确保手动关闭图像平滑或像素化效果不起作用。

无透明像素

对于没有透明像素的画布,它是一个更简单的解决方案,以下函数将执行此操作。要将sctx作为源的画布进行像素化,dctx是目标,pixelCount是目标x轴中的像素块数。你得到filter选项作为奖励,因为GPU正在为你做出艰苦的工作。

请注意,您应检查2D上下文imageSmoothingEnabled的供应商前缀,并将其添加到您打算支持的浏览器中。

// pixelate where
// sctx is source context
// dctx is destination context
// pixelCount is the number of pixel blocks across in the destination
// filter boolean if true then pixel blocks as bilinear interpolation of all pixels involved
//                if false then pixel blocks are nearest neighbour of pixel in center of block


function pixelate(sctx, dctx, pixelCount, filter){
     var sw = sctx.canvas.width;
     var sh = sctx.canvas.height;
     var dw = dctx.canvas.width;
     var dh = dctx.canvas.height;
     var downScale = pixelCount / sw; // get the scale reduction
     var pixH = Math.floor(sh * downScale); // get pixel y axis count

     // clear destination
     dctx.clearRect(0, 0, dw, dh);  
     // set the filter mode
     dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = filter;
     // scale image down;
     dctx.drawImage(sctx.canvas, 0, 0, pixelCount, pixH);

     // scale image back up
     // IMPORTANT for this to work you must turn off smoothing
     dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = false;    
     dctx.drawImage(dctx.canvas, 0, 0, pixelCount, pixH, 0, 0, dw, dh);

     // restore smoothing assuming it was on.
     dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = true;
     //all done 
}

透明像素

如果您有透明像素,则需要工作画布来保持缩小的图像。为此,添加workCanvas参数并返回相同的参数。它会为你创建一个画布,并在需要时调整它的大小,但是你也应该保留它的副本,这样你就不必在每次像素化时不必要地创建一个新画面

function pixelate(sctx, dctx, pixelCount, filter, workCanvas){
     var sw = sctx.canvas.width;
     var sh = sctx.canvas.height;
     var dw = dctx.canvas.width;
     var dh = dctx.canvas.height;
     var downScale = pixelCount / sw; // get the scale reduction
     var pixH = Math.floor(sh * downScale); // get pixel y axis count

     // create a work canvas if needed
     if(workCanvas === undefined){
        workCanvas = document.createElement("canvas"); 
        workCanvas.width = pixelCount ;
        workCanvas.height = pixH; 
        workCanvas.ctx = workCanvas.getContext("2d");
     }else // resize if needed
     if(workCanvas.width !== pixelCount || workCanvas.height !== pixH){
        workCanvas.width = pixelCount ;
        workCanvas.height = pixH; 
        workCanvas.ctx = workCanvas.getContext("2d");
     }
     // clear the workcanvas
     workCanvas.ctx.clearRect(0, 0, pixelCount, pixH);  
     // set the filter mode Note the prefix, and I have been told same goes for IE
     workCanvas.ctx.mozImageSmoothingEnabled = workCanvas.ctx.imageSmoothingEnabled = filter;
     // scale image down;
     workCanvas.ctx.drawImage(sctx.canvas, 0, 0, pixelCount, pixH);

     // clear the destination
     dctx.clearRect(0,0,dw,dh);
     // scale image back up
     // IMPORTANT for this to work you must turn off smoothing
     dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = false;    
     dctx.drawImage(workCanvas, 0, 0, pixelCount, pixH, 0, 0, dw, dh);

     // restore smoothing assuming it was on.
     dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = true;
     //all done 
     return workCanvas; // Return the canvas reference so there is no need to recreate next time
}

要使用透明版本,您需要保存workCanvas referance,否则每次都需要创建一个新版本。

var pixelateWC; // create and leave undefined in the app global scope 
               // (does not have to be JS context global) just of your app 
               // so you dont loss it between renders.

然后在你的主循环中

pixelateWC = pixelate(sourceCtx,destContext,20,true, pixelateWC);

因此该函数将在第一次创建它,然后将反复使用它,直到它需要更改其大小或删除它

pixelateWC = undefined;

演示

我已经包含了一个演示(因为我的原始版本有一个错误),以确保它一切正常。显示函数的透明版本。工作得很好全屏60fp我做整个画布而不仅仅是分割部分。演示中的说明。

&#13;
&#13;
// adapted from QuickRunJS environment. 

// simple mouse
var mouse = (function(){
    function preventDefault(e) { e.preventDefault(); }
    var mouse = {
        x : 0, y : 0, buttonRaw : 0,
        bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
        mouseEvents : "mousemove,mousedown,mouseup".split(",")
    };
    function mouseMove(e) {
        var t = e.type, m = mouse;
        m.x = e.offsetX; m.y = e.offsetY;
        if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
        if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
        } else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];}
        e.preventDefault();
    }
    mouse.start = function(element, blockContextMenu){
        if(mouse.element !== undefined){ mouse.removeMouse();}
        mouse.element = element;
        mouse.mouseEvents.forEach(n => { element.addEventListener(n, mouseMove); } );
        if(blockContextMenu === true){
            element.addEventListener("contextmenu", preventDefault, false);
            mouse.contextMenuBlocked = true;
        }        
    }
    mouse.remove = function(){
        if(mouse.element !== undefined){
            mouse.mouseEvents.forEach(n => { mouse.element.removeEventListener(n, mouseMove); } );
            if(mouse.contextMenuBlocked === true){ mouse.element.removeEventListener("contextmenu", preventDefault);}
            mouse.contextMenuBlocked = undefined;            
            mouse.element = undefined;
        }
    }
    return mouse;
})();

// delete needed for my QuickRunJS environment
function removeCanvas(){
    if(canvas !== undefined){
        document.body.removeChild(canvas);
    }
    canvas = undefined;    
    canvasB = undefined;    
    canvasP = undefined;    
}
// create onscreen, background, and pixelate canvas
function createCanvas(){
    canvas = document.createElement("canvas"); 
    canvas.style.position = "absolute";
    canvas.style.left     = "0px";
    canvas.style.top      = "0px";
    canvas.style.zIndex   = 1000;
    document.body.appendChild(canvas);
    canvasP = document.createElement("canvas"); 
    canvasB = document.createElement("canvas"); 
}
function resizeCanvas(){
    if(canvas === undefined){ createCanvas(); }
    canvasB.width = canvasP.width = canvas.width = window.innerWidth;
    canvasB.height = canvasP.height = canvas.height = window.innerHeight; 
    canvasB.ctx  = canvasB.getContext("2d"); 
    canvasP.ctx  = canvasP.getContext("2d"); 
    canvas.ctx = canvas.getContext("2d"); 
    // lazy coder bug fix 
    joinPos = Math.floor(window.innerWidth/2);
}

function pixelate(sctx, dctx, pixelCount, filter, workCanvas){
     var sw = sctx.canvas.width;
     var sh = sctx.canvas.height;
     var dw = dctx.canvas.width;
     var dh = dctx.canvas.height;
     var downScale = pixelCount / sw; // get the scale reduction
     var pixH = Math.floor(sh * downScale); // get pixel y axis count

     // create a work canvas if needed
     if(workCanvas === undefined){
        workCanvas = document.createElement("canvas"); 
        workCanvas.width = pixelCount;
        workCanvas.height = pixH; 
        workCanvas.ctx = workCanvas.getContext("2d");
     }else // resize if needed
     if(workCanvas.width !== pixelCount || workCanvas.height !== pixH){
        workCanvas.width = pixelCount;
        workCanvas.height = pixH; 
        workCanvas.ctx = workCanvas.getContext("2d");
     }
     // clear the workcanvas
     workCanvas.ctx.clearRect(0, 0, pixelCount, pixH);  
     // set the filter mode
     workCanvas.ctx.mozImageSmoothingEnabled = workCanvas.ctx.imageSmoothingEnabled = filter;
     // scale image down;
     workCanvas.ctx.drawImage(sctx.canvas, 0, 0, pixelCount, pixH);
     
     // clear the destination
     dctx.clearRect(0,0,dw,dh);
     // scale image back up
     // IMPORTANT for this to work you must turn off smoothing
     dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = false;    
     dctx.drawImage(workCanvas, 0, 0, pixelCount, pixH, 0, 0, dw, dh);

     // restore smoothing assuming it was on.
     dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = true;
     //all done 
     return workCanvas; // Return the canvas reference so there is no need to recreate next time
}



var canvas,canvaP, canvasB;
// create and size canvas
resizeCanvas();
// start mouse listening to canvas
mouse.start(canvas,true); // flag that context needs to be blocked
// listen to resize
window.addEventListener("resize",resizeCanvas);

// get some colours
const NUM_COLOURS = 10;
var currentCol = 0;
var cols = (function(count){
    for(var i = 0, a = []; i < count; i ++){
        a.push("hsl("+Math.floor((i/count)*360)+",100%,50%)");
    }
    return a;
})(NUM_COLOURS);
var holdExit = 0; // To stop in QuickRunJS environment
var workCanvas;
var joinPos = Math.floor(canvas.width / 2);
var mouseOverJoin = false;
var dragJoin = false;
var drawing = false;
var filterChange = 0;
var filter = true;
ctx = canvas.ctx;
function update(time){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.drawImage(canvasB, 0, 0, joinPos, canvas.height, 0 , 0, joinPos, canvas.height); // draw background
    ctx.drawImage(canvasP,
        joinPos, 0,
        canvas.width - joinPos, canvas.height, 
        joinPos, 0, 
        canvas.width - joinPos, canvas.height
    ); // pixilation background
    
    if(dragJoin){
        if(!(mouse.buttonRaw & 1)){
            dragJoin = false;
            canvas.style.cursor = "default";
            if(filterChange < 20){
                filter = !filter;
            }
        }else{
            joinPos = mouse.x;
        }
        filterChange += 1;
        mouseOverJoin  = true;
    }else{
        if(Math.abs(mouse.x - joinPos) < 4 && ! drawing){
            if(mouse.buttonRaw & 1){
                canvas.style.cursor = "none";
                dragJoin = true;
                filterChange = 0;
            }else{
                canvas.style.cursor = "e-resize";
            }
            mouseOverJoin  = true;
        }else{
            canvas.style.cursor = "default";
            mouseOverJoin  = false;
            if(mouse.buttonRaw & 1){
                canvasB.ctx.fillStyle = cols[currentCol % NUM_COLOURS];
                canvasB.ctx.beginPath();
                canvasB.ctx.arc(mouse.x, mouse.y, 20, 0, Math.PI * 2);
                canvasB.ctx.fill();
                drawing = true
            }else{
                drawing = false;
            }
        }
        ctx.fillStyle = cols[currentCol % NUM_COLOURS];
        ctx.beginPath();
        ctx.arc(mouse.x, mouse.y, 20, 0, Math.PI * 2);
        ctx.fill();
    }
    if(mouse.buttonRaw & 4){ // setp cols on right button
        currentCol += 1;
    }
    if(mouse.buttonRaw & 2){ // setp cols on right button
        canvasB.ctx.clearRect(0, 0, canvas.width, canvas.height);
        holdExit += 1;
    }else{
        holdExit = 0;
    }
    workCanvas = pixelate(canvasB.ctx, canvasP.ctx, 30, filter, workCanvas);
    ctx.strokeStyle = "rgba(0,0,0,0.5)";
    if(mouseOverJoin){    
        ctx.fillStyle = "rgba(0,255,0,0.5)";
        ctx.fillRect(joinPos - 3, 0, 6, canvas.height);
        ctx.fillRect(joinPos - 2, 0, 4, canvas.height);
        ctx.fillRect(joinPos - 1, 0, 2, canvas.height);
    }
    ctx.strokeRect(joinPos - 3, 0, 6, canvas.height);
    
    ctx.font = "18px arial";
    ctx.textAlign = "left";
    ctx.fillStyle = "black"
    ctx.fillText("Normal canvas.", 10, 20)
    ctx.textAlign = "right";
    ctx.fillText("Pixelated canvas.", canvas.width - 10, 20);
    
    ctx.textAlign = "center";
    ctx.fillText("Click drag to move join.", joinPos, canvas.height - 62);
    ctx.fillText("Click to change Filter : " + (filter ? "Bilinear" : "Nearest"), joinPos, canvas.height - 40);

    ctx.fillText("Click drag to draw.", canvas.width / 2, 20);
    ctx.fillText("Right click change colour.", canvas.width / 2, 42);
    ctx.fillText("Middle click to clear.", canvas.width / 2, 64);

    if(holdExit < 60){
        requestAnimationFrame(update);
    }else{
        removeCanvas();
    }
}
requestAnimationFrame(update);
&#13;
&#13;
&#13;

答案 1 :(得分:2)

为每个像素获取一个ImageData对象,然后构造一个颜色字符串,该字符串必须由canvas对象再次解析,然后使用画布填充例程,该例程必须遍历所有画布设置(转换矩阵,组成等。)

你似乎也画出了比他们需要的更大的矩形:fillRect的第三和第四个参数是宽度和高度,而不是xy协调的右下角。你为什么从像素1开始,而不是从零开始?

对像素操作进行原始数据操作通常要快得多。将整个图像作为图像数据获取,对其进行操作并最终将其放在目标画布上:

var idata = sctx.getImageData(0, 0, w, h);
var data = idata.data;

var wmax = ((w / blockSize) | 0) * blockSize;
var wrest = w - wmax;

var hmax = ((h / blockSize) | 0) * blockSize;
var hrest = h - hmax;

var hh = blockSize;

for (var y = 0; y < h; y += blockSize) {
    var ww = blockSize;
    if (y == hmax) hh = hrest;

    for (var x = 0; x < w; x += blockSize) {
        var n = 4 * (w * y + x);
        var r = data[n];
        var g = data[n + 1];
        var b = data[n + 2];
        var a = data[n + 3];

        if (x == wmax) ww = wrest;

        for (var j = 0; j < hh; j++) {
            var m = n + 4 * (w * j);

            for (var i = 0; i < ww; i++) {
                data[m++] = r;
                data[m++] = g;
                data[m++] = b;
                data[m++] = a;
            }
        }
    }
}

dctx.putImageData(idata, 0, 0);

在我的浏览器中,即使有两个额外的内部循环,它也会更快。