如何对透明图像执行每个像素的碰撞测试? [关闭]

问题描述 投票:1回答:1

如果我有两个部分透明的图像(GIF,PNG,SVG等),如何检查图像的非透明区域是否相交?

如果有必要,我可以使用画布。该解决方案需要使用支持透明性的所有图像格式。请不要使用jQuery。

Touching

Not Touching

javascript image dom image-processing canvas
1个回答
1
投票

使用2D API的快速GPU辅助的像素/像素碰撞。

通过使用2D上下文globalCompositeOperation,可以大大提高像素像素重叠测试的速度。

目的地

comp操作"destination-in"将只在画布上留下可见的像素,而您在其上面绘制的图像也将保留。这样,您可以创建画布,绘制一个图像,然后将comp操作设置为"destination-in",然后绘制第二个图像。如果任何像素重叠,则它们将具有非零的alpha。然后,您要做的就是读取像素,如果其中任何一个都不为零,则说明存在重叠。

更多速度

测试重叠区域中的所有像素会很慢。您可以让GPU为您做一些数学运算并将合成图像缩小。由于像素仅为8位值,因此会有一些损失。可以通过逐步缩小图像并多次渲染结果来克服此问题。每次减少都像计算平均值。我按比例缩小了8,实际上得到了64个像素的平均值。为了避免由于舍入而使范围底部的像素消失,我绘制了几次图像。我执行了32次,其效果是将alpha通道乘以32。

扩展中

可以轻松修改此方法,以使两个图像都可以缩放,倾斜和旋转,而不会影响性能。您也可以使用它来测试许多图像,如果所有图像的像素重叠,则返回true。

像素很小,因此如果在函数中创建测试画布之前减小图像尺寸,则可以提高速度。这可以大大提高性能。

有一个标志reuseCanvas,允许您重新使用工作画布。如果您大量使用测试功能(每秒多次),则将标志设置为true。如果您现在只需要测试,然后将其设置为false。

限制

此方法适用于需要偶尔测试的大图像;对于较小的图像和每帧许多测试(例如在游戏中可能需要测试100张图像的游戏)来说,它不是很好。有关快速(几乎完美像素)碰撞测试的信息,请参见Radial Perimeter Test


作为功能的测试。

// Use the options to set quality of result
// Slow but perfect
var  slowButPerfect = false;
// if reuseCanvas is true then the canvases are resused saving some time
const reuseCanvas = true;
// hold canvas references.
var pixCanvas;
var pixCanvas1;

// returns true if any pixels are overlapping
// img1,img2 the two images to test
// x,y location of img1
// x1,y1 location of img2
function isPixelOverlap(img1,x,y,img2,x1,y1){
    var ax,aw,ay,ah,ctx,canvas,ctx1,canvas1,i,w,w1,h,h1;
    w = img1.width;
    h = img1.height;
    w1 = img2.width;
    h1 = img2.height;
    // function to check if any pixels are visible
    function checkPixels(context,w,h){    
        var imageData = new Uint32Array(context.getImageData(0,0,w,h).data.buffer);
        var i = 0;
        // if any pixel is not zero then there must be an overlap
        while(i < imageData.length){
            if(imageData[i++] !== 0){
                return true;
            }
        }
        return false;
    }

    // check if they overlap
    if(x > x1 + w1 || x + w < x1 || y > y1 + h1 || y + h < y1){
        return false; // no overlap 
    }
    // size of overlapping area
    // find left edge
    ax = x < x1 ? x1 : x;
    // find right edge calculate width
    aw = x + w < x1 + w1 ? (x + w) - ax : (x1 + w1) - ax
    // do the same for top and bottom
    ay = y < y1 ? y1 : y;
    ah = y + h < y1 + h1 ? (y + h) - ay : (y1 + h1) - ay

    // Create a canvas to do the masking on
    if(!reuseCanvas || pixCanvas === undefined){
        pixCanvas = document.createElement("canvas");

    }
    pixCanvas.width = aw;
    pixCanvas.height = ah;
    ctx = pixCanvas.getContext("2d");

    // draw the first image relative to the overlap area
    ctx.drawImage(img1,x - ax, y - ay);

    // set the composite operation to destination-in
    ctx.globalCompositeOperation = "destination-in"; // this means only pixels
                                                     // will remain if both images
                                                     // are not transparent
    ctx.drawImage(img2,x1 - ax, y1 - ay);
    ctx.globalCompositeOperation = "source-over"; 

    // are we using slow method???
    if(slowButPerfect){
        if(!reuseCanvas){  // are we keeping the canvas
            pixCanvas = undefined; // no then release referance
        }
        return checkPixels(ctx,aw,ah);
    }

    // now draw over its self to amplify any pixels that have low alpha
    for(var i = 0; i < 32; i++){
        ctx.drawImage(pixCanvas,0,0);
    }
    // create a second canvas 1/8th the size but not smaller than 1 by 1
    if(!reuseCanvas || pixCanvas1 === undefined){
        pixCanvas1 = document.createElement("canvas");
    }
    ctx1 = pixCanvas1.getContext("2d");
    // reduced size rw, rh
    rw = pixCanvas1.width = Math.max(1,Math.floor(aw/8));
    rh = pixCanvas1.height = Math.max(1,Math.floor(ah/8));
    // repeat the following untill the canvas is just 64 pixels
    while(rw > 8 && rh > 8){
        // draw the mask image several times
        for(i = 0; i < 32; i++){
            ctx1.drawImage(
                pixCanvas,
                0,0,aw,ah,
                Math.random(),
                Math.random(),
                rw,rh
            );
        }
        // clear original
        ctx.clearRect(0,0,aw,ah);
        // set the new size
        aw = rw;
        ah = rh;
        // draw the small copy onto original
        ctx.drawImage(pixCanvas1,0,0);
        // clear reduction canvas
        ctx1.clearRect(0,0,pixCanvas1.width,pixCanvas1.height);
        // get next size down
        rw = Math.max(1,Math.floor(rw / 8));
        rh = Math.max(1,Math.floor(rh / 8));
    }
    if(!reuseCanvas){ // are we keeping the canvas
        pixCanvas = undefined;  // release ref
        pixCanvas1 = undefined;
    }
    // check for overlap
    return checkPixels(ctx,aw,ah);
}

演示(使用整页)

该演示使您可以比较这两种方法。显示每个测试的平均时间。 (如果未完成测试,将显示NaN)

为了获得最佳结果,请观看演示全页。

使用鼠标左键或鼠标右键测试是否重叠。将splat图像移到另一个图像上,以查看重叠结果。在我的计算机上,慢速测试大约需要11毫秒,快速测试大约需要0.03毫秒(使用Chrome,在Firefox上更快)。

我没有花太多时间测试我可以使其工作的速度,但是有很多空间可以通过减少图像相互绘制的时间来提高速度。在某些时候,微弱的像素会丢失。

// Use the options to set quality of result
// Slow but perfect
var  slowButPerfect = false;
const reuseCanvas = true;
var pixCanvas;
var pixCanvas1;

// returns true if any pixels are overlapping
function isPixelOverlap(img1,x,y,w,h,img2,x1,y1,w1,h1){
    var ax,aw,ay,ah,ctx,canvas,ctx1,canvas1,i;
    // function to check if any pixels are visible
    function checkPixels(context,w,h){    
        var imageData = new Uint32Array(context.getImageData(0,0,w,h).data.buffer);
        var i = 0;
        // if any pixel is not zero then there must be an overlap
        while(i < imageData.length){
            if(imageData[i++] !== 0){
                return true;
            }
        }
        return false;
    }
    
    // check if they overlap
    if(x > x1 + w1 || x + w < x1 || y > y1 + h1 || y + h < y1){
        return false; // no overlap 
    }
    // size of overlapping area
    // find left edge
    ax = x < x1 ? x1 : x;
    // find right edge calculate width
    aw = x + w < x1 + w1 ? (x + w) - ax : (x1 + w1) - ax
    // do the same for top and bottom
    ay = y < y1 ? y1 : y;
    ah = y + h < y1 + h1 ? (y + h) - ay : (y1 + h1) - ay
    
    // Create a canvas to do the masking on
    if(!reuseCanvas || pixCanvas === undefined){
        pixCanvas = document.createElement("canvas");
        
    }
    pixCanvas.width = aw;
    pixCanvas.height = ah;
    ctx = pixCanvas.getContext("2d");
    
    // draw the first image relative to the overlap area
    ctx.drawImage(img1,x - ax, y - ay);
    
    // set the composite operation to destination-in
    ctx.globalCompositeOperation = "destination-in"; // this means only pixels
                                                     // will remain if both images
                                                     // are not transparent
    ctx.drawImage(img2,x1 - ax, y1 - ay);
    ctx.globalCompositeOperation = "source-over"; 
    
    // are we using slow method???
    if(slowButPerfect){
        if(!reuseCanvas){  // are we keeping the canvas
            pixCanvas = undefined; // no then release reference
        }
        return checkPixels(ctx,aw,ah);
    }
    
    // now draw over its self to amplify any pixels that have low alpha
    for(var i = 0; i < 32; i++){
        ctx.drawImage(pixCanvas,0,0);
    }
    // create a second canvas 1/8th the size but not smaller than 1 by 1
    if(!reuseCanvas || pixCanvas1 === undefined){
        pixCanvas1 = document.createElement("canvas");
    }
    ctx1 = pixCanvas1.getContext("2d");
    // reduced size rw, rh
    rw = pixCanvas1.width = Math.max(1,Math.floor(aw/8));
    rh = pixCanvas1.height = Math.max(1,Math.floor(ah/8));
    // repeat the following untill the canvas is just 64 pixels
    while(rw > 8 && rh > 8){
        // draw the mask image several times
        for(i = 0; i < 32; i++){
            ctx1.drawImage(
                pixCanvas,
                0,0,aw,ah,
                Math.random(),
                Math.random(),
                rw,rh
            );
        }
        // clear original
        ctx.clearRect(0,0,aw,ah);
        // set the new size
        aw = rw;
        ah = rh;
        // draw the small copy onto original
        ctx.drawImage(pixCanvas1,0,0);
        // clear reduction canvas
        ctx1.clearRect(0,0,pixCanvas1.width,pixCanvas1.height);
        // get next size down
        rw = Math.max(1,Math.floor(rw / 8));
        rh = Math.max(1,Math.floor(rh / 8));
    }
    if(!reuseCanvas){ // are we keeping the canvas
        pixCanvas = undefined;  // release ref
        pixCanvas1 = undefined;
    }
    // check for overlap
    return checkPixels(ctx,aw,ah);
}

function rand(min,max){
    if(max === undefined){
        max = min;
        min = 0;
    }
    var r = Math.random() + Math.random() + Math.random() + Math.random() + Math.random();
    r += Math.random() + Math.random() + Math.random() + Math.random() + Math.random();
    r /= 10;
    return (max-min) * r + min;
}
function createImage(w,h){
    var c = document.createElement("canvas");
    c.width = w;
    c.height = h;
    c.ctx = c.getContext("2d");
    return c;
}

function createCSSColor(h,s,l,a) {
      var col = "hsla(";
      col += (Math.floor(h)%360) + ",";
      col += Math.floor(s) + "%,";
      col += Math.floor(l) + "%,";
      col += a + ")";
      return col;
}
function createSplat(w,h,hue, hue2){
    w = Math.floor(w);
    h = Math.floor(h);
    var c = createImage(w,h);
    if(hue2 !== undefined) {
        c.highlight = createImage(w,h);
    }
    var maxSize = Math.min(w,h)/6;
    var pow = 5;
    while(maxSize > 4 && pow > 0){
        var count = Math.min(100,Math.pow(w * h,1/pow) / 2);
        while(count-- > 0){
            
            const rhue = rand(360);
            const s = rand(25,75);
            const l = rand(25,75);
            const a = (Math.random()*0.8+0.2).toFixed(3);
            const size = rand(4,maxSize);
            const x = rand(size,w - size);
            const y = rand(size,h - size);
            
            c.ctx.fillStyle = createCSSColor(rhue  + hue, s, l, a);
            c.ctx.beginPath();
            c.ctx.arc(x,y,size,0,Math.PI * 2);
            c.ctx.fill();
            if (hue2 !== undefined) {
                c.highlight.ctx.fillStyle = createCSSColor(rhue  + hue2, s, l, a);
                c.highlight.ctx.beginPath();
                c.highlight.ctx.arc(x,y,size,0,Math.PI * 2);
                c.highlight.ctx.fill();
            }
            
        }
        pow -= 1;
        maxSize /= 2;
    }
    return c;
}
var splat1,splat2;
var slowTime = 0;
var slowCount = 0;
var notSlowTime = 0;
var notSlowCount = 0;
var onResize = function(){
    ctx.font = "14px arial";
    ctx.textAlign = "center";
    splat1 = createSplat(rand(w/2, w), rand(h/2, h), 0, 100);
    splat2 = createSplat(rand(w/2, w), rand(h/2, h), 100);
}
function display(){
    ctx.clearRect(0,0,w,h)
    ctx.setTransform(1.8,0,0,1.8,w/2,0);
    ctx.fillText("Fast GPU assisted Pixel collision test using 2D API",0, 14)
    ctx.setTransform(1,0,0,1,0,0);
    ctx.fillText("Hold left mouse for Traditional collision test. Time : " + (slowTime / slowCount).toFixed(3) + "ms",w /2 , 28 + 14)
    ctx.fillText("Hold right (or CTRL left) mouse for GPU assisted collision. Time: "+ (notSlowTime / notSlowCount).toFixed(3) + "ms",w /2 , 28 + 28)
    if((mouse.buttonRaw & 0b101) === 0) {
        ctx.drawImage(splat1, w / 2 - splat1.width / 2, h / 2 - splat1.height / 2)
        ctx.drawImage(splat2, mouse.x - splat2.width / 2, mouse.y - splat2.height / 2);        
    
    } else if(mouse.buttonRaw & 0b101){
        if((mouse.buttonRaw & 1) && !mouse.ctrl){
            slowButPerfect = true;
        }else{
            slowButPerfect = false;
        }
        var now = performance.now();
        var res = isPixelOverlap(
            splat1,
            w / 2 - splat1.width / 2, h / 2 - splat1.height / 2,
            splat1.width, splat1.height,
            splat2, 
            mouse.x - splat2.width / 2, mouse.y - splat2.height / 2,
            splat2.width,splat2.height
        )
        var time = performance.now() - now;
        ctx.drawImage(res ? splat1.highlight:  splat1, w / 2 - splat1.width / 2, h / 2 - splat1.height / 2)
        ctx.drawImage(splat2, mouse.x - splat2.width / 2, mouse.y - splat2.height / 2);        
        
        if(slowButPerfect){
            slowTime += time;
            slowCount += 1;
        }else{
            notSlowTime = time;
            notSlowCount += 1;
        }
        if(res){
            ctx.setTransform(2,0,0,2,mouse.x,mouse.y);
            ctx.fillText("Overlap detected",0,0)
            ctx.setTransform(1,0,0,1,0,0);
        }
        //mouse.buttonRaw = 0;
        
    }


    
}




// Boilerplate code below



const RESIZE_DEBOUNCE_TIME = 100;
var w, h, cw, ch, canvas, ctx, mouse, createCanvas, resizeCanvas, setGlobals, globalTime = 0, resizeCount = 0;
var firstRun = true;
createCanvas = function () {
    var c,
    cs;
    cs = (c = document.createElement("canvas")).style;
    cs.position = "absolute";
    cs.top = cs.left = "0px";
    cs.zIndex = 1000;
    document.body.appendChild(c);
    return c;
}
resizeCanvas = function () {
    if (canvas === undefined) {
        canvas = createCanvas();
    }
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    ctx = canvas.getContext("2d");
    if (typeof setGlobals === "function") {
        setGlobals();
    }
    if (typeof onResize === "function") {
        if(firstRun){
            onResize();
            firstRun = false;
        }else{
            resizeCount += 1;
            setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
        }
    }
}
function debounceResize() {
    resizeCount -= 1;
    if (resizeCount <= 0) {
        onResize();
    }
}
setGlobals = function () {
    cw = (w = canvas.width) / 2;
    ch = (h = canvas.height) / 2;
}
mouse = (function () {
    function preventDefault(e) {
        e.preventDefault();
    }
    var mouse = {
        x : 0,
        y : 0,
        buttonRaw : 0,
        over : false,
        bm : [1, 2, 4, 6, 5, 3],
        active : false,
        bounds : null,
        mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover".split(",")
    };
    var m = mouse;
    function mouseMove(e) {
        var t = e.type;
        m.bounds = m.element.getBoundingClientRect();
        m.x = e.pageX - m.bounds.left;
        m.y = e.pageY - m.bounds.top;
        m.alt = e.altKey;
        m.shift = e.shiftKey;
        m.ctrl = e.ctrlKey;
        if (t === "mousedown") {
            m.buttonRaw |= m.bm[e.which - 1];
        } else if (t === "mouseup") {
            m.buttonRaw &= m.bm[e.which + 2];
        } else if (t === "mouseout") {
            m.buttonRaw = 0;
            m.over = false;
        } else if (t === "mouseover") {
            m.over = true;
        }
        
        e.preventDefault();
    }
    m.start = function (element) {
        if (m.element !== undefined) {
            m.removeMouse();
        }
        m.element = element === undefined ? document : element;
        m.mouseEvents.forEach(n => {
            m.element.addEventListener(n, mouseMove);
        });
        m.element.addEventListener("contextmenu", preventDefault, false);
        m.active = true;
    }
    m.remove = function () {
        if (m.element !== undefined) {
            m.mouseEvents.forEach(n => {
                m.element.removeEventListener(n, mouseMove);
            });
            m.element = undefined;
            m.active = false;
        }
    }
    return mouse;
})();

resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);

function update1(timer) { // Main update loop
    if(ctx === undefined){
        return;
    }
    globalTime = timer;
    display(); // call demo code
    requestAnimationFrame(update1);
}
requestAnimationFrame(update1);
© www.soinside.com 2019 - 2024. All rights reserved.