如何对透明图像进行每个像素碰撞测试



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

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

触摸

不触摸

使用2D API。

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

目的地

COMP操作"destination-in"仅留下在画布上可见的像素和您在其顶部绘制的图像。因此,您创建一个画布,绘制一个图像,然后将Comp操作设置为"destination-in",然后绘制第二张图像。如果任何像素是重叠的,那么它们将具有非零alpha。然后,您要做的就是阅读像素,如果其中任何一个不是零,您就会知道有重叠。

更多速度

测试重叠区域中的所有像素都会很慢。您可以让GPU为您进行一些数学,并将复合图像缩小。由于像素仅是8位值,因此有一些损失。可以通过以步骤减少图像并多次渲染结果来克服这一点。每个还原就像计算平均值。我有效地缩小了8个平均值64像素。为了在范围的底部停止像素,由于四舍五入,我几次绘制图像。我这样做了32个时间,它具有将alpha通道乘以32的效果。

扩展

可以轻松地修改此方法,以允许在没有任何重大性能的情况下将两个图像缩放,偏斜和旋转。您也可以使用它来测试许多图像,如果所有图像都有像素重叠,则可以返回true。

像素很小,因此,如果在功能中创建测试画布之前,请减小图像大小,可以获得额外的速度。这可以带来显着的性能。

有一个标志reuseCanvas,它允许您重复使用工作画布。如果您经常使用测试功能(每秒很多次),则将标志设置为true。如果您时不时需要测试,然后将其设置为false。

限制

此方法适用于需要偶尔测试的大图像;它不适用于小图像和每个框架的许多测试(例如,在您可能需要测试100张图像的游戏中)。有关快速(几乎完美的像素)碰撞测试,请参见径向周边测试。


测试作为功能。

// 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毫秒,快速测试(使用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);

最新更新