如何检查两个部分透明的图像是否相互交叉/相互接触?

时间:2016-12-03 22:04:01

标签: javascript image dom image-processing canvas

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

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

Touching

Not Touching

1 个答案:

答案 0 :(得分:1)

使用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 如果您找到有用的答案,请不要忘记upvote。

测试作为一种功能。

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

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 createSplat(w,h,hue){
    w = Math.floor(w);
    h = Math.floor(h);
    var c = createImage(w,h);
    var maxSize = Math.min(w,h)/8;
    var pow = 5;
    while(maxSize > 4 && pow > 0){
        var count = Math.min(100,Math.pow(w * h,1/pow) / 2);
        while(count-- > 0){
            var col = "hsla(";
            col += (Math.floor(rand(360)+hue)%360) + ",";
            col += Math.floor(rand(25,75)) + "%,";
            col += Math.floor(rand(25,75)) + "%,";
            col += (Math.random()*0.8+0.2).toFixed(3) + ")";
            var size = rand(4,maxSize);
            c.ctx.fillStyle = col;
            c.ctx.beginPath();
            c.ctx.arc(rand(size,w-size*2),rand(size,h - size*2),size,0,Math.PI * 2);
            c.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);
    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("Pixel V pixel test using GPU and 2D Context",0, 14)
    ctx.setTransform(1,0,0,1,0,0);
    ctx.fillText("Hold Left button for slow and right for faster test",w /2 , 38)
    ctx.fillText("Perfect pixels. Mean time : " + (slowTime / slowCount).toFixed(3) + "ms",w /2 , 38 + 14)
    ctx.fillText("Almost Perfect pixels. Mean time : "+ (notSlowTime / notSlowCount).toFixed(3) + "ms",w /2 , 38 + 28)
    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);
    if(mouse.buttonRaw & 0b101){
        if(mouse.buttonRaw & 1){
            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;
        if(mouse.buttonRaw & 1){
            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);