我在使用另一个画布/图像(png)中的画布putImageData复制透明像素时遇到了麻烦 [英] I'm having troubles with copying transparent pixels with canvas putImageData from another canvas/image(png)

查看:138
本文介绍了我在使用另一个画布/图像(png)中的画布putImageData复制透明像素时遇到了麻烦的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试复制上方的圆圈看到僵尸圈的边缘。



(问题:黑色重叠区域代替透明区域,通过上面的圆圈可以看到僵尸圈的边缘)



我希望我尽可能清楚地表达自己(上图显示)我的问题非常清楚)我想感谢你的帮助。



绘制功能 - 最终绘制到可见画布。

  Game.prototype.draw2 = function(interpolation,canvas,ctx,group)
{
var canvasData = ctx.createImageData(canvas.width, canvas.height),
cDa ta = canvasData.data;

for(var i = 0; i< group.length; i ++)
{
var obj = group [i];

如果(!obj.draw)
{
继续;
}

var imagePixelData = obj.imagePixelData;

var x = obj.previous.x +(obj.x - obj.previous.x)* interpolation;
var y = obj.previous.y +(obj.y - obj.previous.y)*插值;

for(var w = 0; w< obj.width; w ++)
{
for(var h = 0; h< obj.height; h ++)
{
if(x + w< canvas.width&& obj.x + w> 0&&
y + h> 0&& y + h < canvas.height)
{
var iData =(h * obj.width + w)* 4;
var pData =(~~(x + w)+ ~~(y + h)* canvas.width)* 4;

cData [pData] = imagePixelData [iData];
cData [pData + 1] = imagePixelData [iData + 1];
cData [pData + 2] = imagePixelData [iData + 2];
if(cData [pData + 3]< 100)
{
cData [pData + 3] = imagePixelData [iData + 3];
}

}
}
}
}
ctx.putImageData(canvasData,0,0);
};

以下是我在其他隐形画布中准备粉红色圆形区域的方法。

  Game.prototype.constructors.Attractor.prototype.getImageData = function(context)
{
this.separateScene = new context.constructors .Graphics(this.width,this.height,false);
this.image = this.separateScene.canvas;
this.separateScene.ctx.beginPath();
this.separateScene.ctx.arc(this.radius,this.radius,this.radius,0,2 * Math.PI,false);
this.separateScene.ctx.fillStyle ='#ff9b9b';
this.separateScene.ctx.fill();
this.separateScene.ctx.beginPath();
this.separateScene.ctx.arc(this.radius,this.radius,this.radiusCut,0,2 * Math.PI,false);
this.separateScene.ctx.fillStyle ='rgba(255,255,255,0.27)';
this.separateScene.ctx.fill();
this.separateScene.ctx.beginPath();
this.separateScene.ctx.arc(this.radius,this.radius,this.coreRadius,0,2 * Math.PI,false);
this.separateScene.ctx.fillStyle ='#ff64b2';
this.separateScene.ctx.fill();
this.imageData = this.separateScene.ctx.getImageData(0,0,this.width,this.height);
this.imagePixelData = this.imageData.data;
};


解决方案

狩猎黑色像素






@ Loktar对特定图像的答案很棒,仅由黑色和透明像素组成。



在imageData中,这两种类型的像素非常相似,因为只有它们的alpha值不同。所以他的代码只对alpha值进行了绘制检查(每个循环中的第四个)。


  cData [pData] = imagePixData [iData]; 
cData [pData + 1] = imagePixData [iData + 1];
cData [pData + 2] = imagePixData [iData + 2];
//仅检查alpha值...
if(cData [pData + 3]< 100){
cData [pData + 3] = imagePixData [iData + 3];
}


另一方面,你是处理彩色图像。因此,当这个部分针对透明像素执行,并且您已经在此位置处有一个彩色像素时,三个第一行将现有像素转换为透明像素的rgb值( 0,0,0 )但保留现有像素的alpha值(在您的情况下 255 )。



然后你有一个黑色像素而不是之前的彩色像素。



要解决它,你可以在检查当前 imagePixData 的不透明度的条件下包装整个块,而不是检查已经绘制的那个。

  if(imagePixelData [iData + 3]> 150){
cData [pData] = imagePixelData [iData];
cData [pData + 1] = imagePixelData [iData + 1];
cData [pData + 2] = imagePixelData [iData + 2];
cData [pData + 3] = imagePixelData [iData + 3];
}



打击白色






由于抗锯齿,这些白色像素在这里。它已经存在于@ Loktar的原始示例中,由于其图像的大小而不太明显。



当您处理imageData时,这些工件是垃圾,因为我们可以只修改每个像素,并且我们不能在子像素上设置值。换句话说,我们不能使用抗锯齿。



这是< 100 在原始检查中,或上面的解决方案中的> 150



此检查中针对alpha值的最小范围,您将获得的工件越少。但另一方面,你的边界会越粗糙。



你自己找到合适的价值,但圈子是最糟糕的,因为几乎每个边界像素都是消除锯齿。



改善令人敬畏的 (又名我可以得到10000张彩色图像)






您的实际实施让我想到了可以对@ Loktar的解决方案进行的一些改进。



我们可以对每个像素进行第一次循环,而不是保留原始图像的数据,并存储一个由六个插槽组成的新imageData数组: [x,y,r,g, b,a]



通过这种方式,我们可以避免存储我们不想要的所有透明像素,从而减少每次调用的迭代次数,并且我们也可以避免任何alpha检查在每个循环中。最后,我们甚至不需要从图像画布中获取位置像素,因为我们为每个像素存储它。



这是带注释的代码示例作为概念证明。



  var parseImageData = function(ctx){var pixelArr = ctx。 getImageData(0,0,ctx.canvas.width,ctx.canvas.height).data; //我们的图像宽度var w = ctx.canvas.width; //首先存储我们的图像的维度var filtered = []; //循环遍历我们所有图像的像素(var i = 0; i< pixelArr.length; i + = 4){//我们不想要像素或几乎透明的像素if(pixelArr [i + 3]< 250){继续; } //获取像素的实际x y位置var f =(i / 4)/ w; var y = Math.floor(f); var x = Math.round((f  -  y)* w); //将像素添加到我们的数组,其x y位置filtered.push(x,y,pixelArr [i],pixelArr [i + 1],pixelArr [i + 2],pixelArr [i + 3]); } return filtered;}; //这里我们将存储我们所有的像素arrayvar images = []; //这里我们将存储我们的entitiesvar objects = []; var draw = function(){//创建一个新的空imageData main canvas var imageData = mainCtx.createImageData(mainCanvas.width,mainCanvas.height); //获取我们将写入var pixels = imageData.data的数组; var width = mainCanvas.width; var pixelArray,deg = Math.PI / 180; //微优化不会伤害(var n = 0; n< objects.length; n ++){var entity = objects [n],// HERE通过OP velY = Math更新你的对象//一些奇特的东西.cos(entity.angle * deg)* entity.speed,velX = Math.sin(entity.angle * deg)* entity.speed; entity.x + = velX; entity.y  -  = velY; entity.angle ++; // END更新//检索我们在pixelArray = images [entity.image]之前创建的像素数组; //遍历我们的像素数组(var p = 0; p< pixelArray.length; p + = 6){//检索我们像素的x和位置,相对于原始图像var x = pixelArray [p] ; var y = pixelArray [p + 1]; //获取我们(像素+对象)相对于画布大小的位置var pData =(~~(entity.x + x)+ ~~(entity.y + y)* width)* 4 //绘制我们的像素pixels [pData] = pixelArray [p + 2];像素[pData + 1] = pixelArray [p + 3];像素[pData + 2] = pixelArray [p + 4];像素[pData + 3] = pixelArray [p + 5]; } // //一切都在这里,放置图像数据mainCtx.putImageData(imageData,0,0);}; var mainCanvas = document.createElement('canvas'); var mainCtx = mainCanvas.getContext('2d'); mainCanvas .width = 800; mainCanvas.height = 600; document.body.appendChild(mainCanvas); //仅用于demovar colors = ['lightblue','orange','lightgreen','pink']; //画布将用于绘制所有图像并获取其dataImagevar imageCtx = document.createElement('canvas')。getContext('2d'); //绘制随机imagevar randomEgg = function(){if(Math.random() < .8){var radius = Math.random()* 25 + 1; var c = Math.floor(Math.random()* colors.length); var c1 =(c + Math.ceil(Math.random()*(colors.length  -  1)))%(colors.length); imageCtx.canvas.width = imageCtx.canvas.height = radius * 2 + 3; imageCtx.beginPath(); imageCtx.fillStyle = colors [c]; imageCtx.arc(radius,radius,radius,0,Math.PI * 2); imageCtx.fill(); imageCtx.beginPath(); imageCtx.fillStyle = colors [c1]; imageCtx.arc(radius,radius,radius / 2,0,Math.PI * 2); imageCtx.fill(); } else {var img = Math.floor(Math.random()* loadedImage.length); imageCtx.canvas.width = loadedImage [img] .width; imageCtx.canvas.height = loadedImage [img] .height; imageCtx.drawImage(loadedImage [img],0,0); } return parseImageData(imageCtx);}; // init我们的对象和shapesvar init = function(){var i; for(i = 0; i< 30; i ++){images.push(randomEgg()); } for(i = 0; i< 10000; i ++){ob​​jects.push({angle:Math.random()* 360,x:100 +(Math.random()* mainCanvas.width / 2),y: 100 +(Math.random()* mainCanvas.height / 2),速度:1 + Math.random()* 20,图像:Math.floor(Math.random()*(images.length))}); } loop();}; var loop = function(){draw(); requestAnimationFrame(loop);}; //我们的异地图像将是storedvar loadedImage = [];(函数preloadImages(){var toLoad = ['https://dl.dropboxusercontent.com/s/4e90e48s5vtmfbd/aaa.png' ,'https://dl.dropboxusercontent.com/s/rumlhyme6s5f8pt/ABC.png']; for(var i = 0; i< toLoad.length; i ++){var img = new Image(); img.crossOrigin ='anonymous'; img.onload = function(){loadedImage.push(this); if(loadedImage.length === toLoad.length){init();}}; img.src = toLoad [i];} })();  



请注意,你的要绘制的图像将是最慢的图纸。


I'm trying to copy method discribed here on stackoverflow. But I'm having some problems which I don't know how to solve.

I set up jsfiddle to demonstrate everything. Here is the second jsfiddle with only particles moving and being drawn.

My problem lies in drawing, profiler showed that with around 10000 particles drawImage takes 40% of overall loop time. Without drawing directly and only calculations nothing hinders code exectuion so problem lies in drawing.


Is there a way how to use this technique without these side effects? Currently I show you how I create circle areas with arc but I also use png files for some other objects and they exhibit exactle the same behaviour.

(problem: black overlapping area instead of transparent area, bottim circle's edge can be seen through the circle above)

I hope I expressed myself as clearly as possible (picture abovedisplays my problem very clearly) and I would like to thank you for your help.

Draw function - final draw to visible canvas.

Game.prototype.draw2 = function(interpolation, canvas, ctx, group)
{
    var canvasData = ctx.createImageData(canvas.width, canvas.height),
        cData = canvasData.data;

    for (var i = 0; i < group.length; i++)
    {
        var obj = group[i];

        if(!obj.draw)
        {
            continue;
        }

        var imagePixelData = obj.imagePixelData;

        var x = obj.previous.x + (obj.x - obj.previous.x) * interpolation;
        var y = obj.previous.y + (obj.y - obj.previous.y) * interpolation;

        for (var w = 0; w < obj.width; w++)
        {
            for (var h = 0; h < obj.height; h++)
            {
                if (x + w < canvas.width && obj.x + w > 0 &&
                    y + h > 0 && y + h < canvas.height)
                {
                    var iData = (h * obj.width + w) * 4;
                    var pData = (~~ (x + w) + ~~ (y + h) * canvas.width) * 4;

                    cData[pData] = imagePixelData[iData];
                    cData[pData + 1] = imagePixelData[iData + 1];
                    cData[pData + 2] = imagePixelData[iData + 2];
                    if (cData[pData + 3] < 100)
                    {
                        cData[pData + 3] = imagePixelData[iData + 3];
                    }

                }
            }
        }    
    }
    ctx.putImageData(canvasData, 0, 0);
};

And here is how I prepare pinkish circular area in other invisible canvas.

Game.prototype.constructors.Attractor.prototype.getImageData = function(context)
{
    this.separateScene = new context.constructors.Graphics(this.width, this.height, false);
    this.image = this.separateScene.canvas;
    this.separateScene.ctx.beginPath();
    this.separateScene.ctx.arc(this.radius, this.radius, this.radius, 0, 2 * Math.PI, false);
    this.separateScene.ctx.fillStyle = '#ff9b9b';
    this.separateScene.ctx.fill();
    this.separateScene.ctx.beginPath();
    this.separateScene.ctx.arc(this.radius, this.radius, this.radiusCut, 0, 2 * Math.PI, false);
    this.separateScene.ctx.fillStyle = 'rgba(255, 255, 255, 0.27)';
    this.separateScene.ctx.fill();
    this.separateScene.ctx.beginPath();
    this.separateScene.ctx.arc(this.radius, this.radius, this.coreRadius, 0, 2 * Math.PI, false);
    this.separateScene.ctx.fillStyle = '#ff64b2';
    this.separateScene.ctx.fill();
    this.imageData = this.separateScene.ctx.getImageData(0, 0, this.width, this.height);
    this.imagePixelData = this.imageData.data;
};

解决方案

Hunting the black pixels


@Loktar's great answer was made for a particular image, only composed of black and transparent pixels.

In the imageData, these two type of pixels are very similar, since only their alpha value differ. So his code was only doing a should-draw check over the alpha value (the fourth in each loop).

cData[pData] = imagePixData[iData];
cData[pData + 1] = imagePixData[iData + 1];
cData[pData + 2] = imagePixData[iData + 2];
// only checking for the alpha value...
if(cData[pData + 3] < 100){
  cData[pData + 3] = imagePixData[iData + 3];
}

You, in the other hand, are dealing with colored images. So when this part is executed against a transparent pixel, and that you already have a colored pixel at this position, the three first lines will convert the existing pixel to the transparent one's rgb values (0,0,0) but leave the alpha value of the existing pixel (in your case 255).

You then have a black pixel instead of the colored one that were here previously.

To solve it, you can wrap the full block in a condition that checks the opacity of the current imagePixData, instead of checking the one already drawn.

if (imagePixelData[iData+3]>150){
    cData[pData] = imagePixelData[iData];
    cData[pData + 1] = imagePixelData[iData + 1];
    cData[pData + 2] = imagePixelData[iData + 2];
    cData[pData + 3] = imagePixelData[iData + 3];
}

Fighting the white ones


Those white pixels are here because of the anti-aliasing. It was already there in @Loktar's original example, simply less visible because of the size of his images.

These artifacts are crap when you do deal with imageData, since we can just modify each pixel, and that we can't set values on sub-pixels. In other words, we can't make use of anti-aliasing.

That's the purpose of the <100 in original checking, or the >150 in my solution above.

The smallest range you will take in this check against the alpha value, the less artifacts you'll get. But in the other hand, the rougher your borders will be.

You ave to find the right value by yourself, but circles are the worst since almost every border pixels will be anti-aliased.

Improving the awesome (a.k.a I can get you 10000 colored images)


Your actual implementation made me think about some improvements that could be made on @Loktar's solution.

Instead of keeping the original image's data, we could do a first loop over every pixels, and store a new imageData array, composed of six slots : [x, y, r, g, b ,a].

This way, we can avoid the storing of all the transparent pixels we don't want, which makes less iterations at each call, and we can also avoid any alpha checking in each loop. Finally, we don't even need to "get the position pixel from the image canvas" since we stored it for each pixel.

Here is an annotated code example as a proof of concept.

var parseImageData = function(ctx) {
  var pixelArr = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;
  // the width of our image
  var w = ctx.canvas.width;
  // first store our image's dimension
  var filtered = [];
  // loop through all our image's pixels
  for (var i = 0; i < pixelArr.length; i += 4) {
    // we don't want traparent or almost transparent pixels
    if (pixelArr[i + 3] < 250) {
      continue;
    }
    // get the actual x y position of our pixel
    var f = (i / 4) / w;
    var y = Math.floor(f);
    var x = Math.round((f - y) * w);
    // add the pixel to our array, with its x y positions
    filtered.push(x, y, pixelArr[i], pixelArr[i + 1], pixelArr[i + 2], pixelArr[i + 3]);
  }

  return filtered;
};
// here we will store all our pixel arrays
var images = [];
// here we will store our entities
var objects = [];

var draw = function() {
  // create a new empty imageData of our main canvas
  var imageData = mainCtx.createImageData(mainCanvas.width, mainCanvas.height);
  // get the array we'll write onto
  var pixels = imageData.data;

  var width = mainCanvas.width;

  var pixelArray,
    deg = Math.PI / 180; // micro-optimizaion doesn't hurt

  for (var n = 0; n < objects.length; n++) {
    var entity = objects[n],
      // HERE update your objects
      // some fancy things by OP
      velY = Math.cos(entity.angle * deg) * entity.speed,
      velX = Math.sin(entity.angle * deg) * entity.speed;

    entity.x += velX;
    entity.y -= velY;

    entity.angle++;
    // END update

    // retrieve	the pixel array we created before
    pixelArray = images[entity.image];

    // loop through our pixel Array
    for (var p = 0; p < pixelArray.length; p += 6) {
      // retrieve the x and positions of our pixel, relative to its original image
      var x = pixelArray[p];
      var y = pixelArray[p + 1];
      // get the position of our ( pixel + object ) relative to the canvas size
      var pData = (~~(entity.x + x) + ~~(entity.y + y) * width) * 4
        // draw our pixel
      pixels[pData] = pixelArray[p + 2];
      pixels[pData + 1] = pixelArray[p + 3];
      pixels[pData + 2] = pixelArray[p + 4];
      pixels[pData + 3] = pixelArray[p + 5];
    }
  }
  // everything is here, put the image data
  mainCtx.putImageData(imageData, 0, 0);
};



var mainCanvas = document.createElement('canvas');
var mainCtx = mainCanvas.getContext('2d');

mainCanvas.width = 800;
mainCanvas.height = 600;

document.body.appendChild(mainCanvas);


// just for the demo
var colors = ['lightblue', 'orange', 'lightgreen', 'pink'];
// the canvas that will be used to draw all our images and get their dataImage
var imageCtx = document.createElement('canvas').getContext('2d');

// draw a random image
var randomEgg = function() {
  if (Math.random() < .8) {
    var radius = Math.random() * 25 + 1;
    var c = Math.floor(Math.random() * colors.length);
    var c1 = (c + Math.ceil(Math.random() * (colors.length - 1))) % (colors.length);
    imageCtx.canvas.width = imageCtx.canvas.height = radius * 2 + 3;
    imageCtx.beginPath();
    imageCtx.fillStyle = colors[c];
    imageCtx.arc(radius, radius, radius, 0, Math.PI * 2);
    imageCtx.fill();
    imageCtx.beginPath();
    imageCtx.fillStyle = colors[c1];
    imageCtx.arc(radius, radius, radius / 2, 0, Math.PI * 2);
    imageCtx.fill();
  } else {
    var img = Math.floor(Math.random() * loadedImage.length);
    imageCtx.canvas.width = loadedImage[img].width;
    imageCtx.canvas.height = loadedImage[img].height;
    imageCtx.drawImage(loadedImage[img], 0, 0);
  }
  return parseImageData(imageCtx);
};

// init our objects and shapes
var init = function() {
  var i;
  for (i = 0; i < 30; i++) {
    images.push(randomEgg());
  }
  for (i = 0; i < 10000; i++) {
    objects.push({
      angle: Math.random() * 360,
      x: 100 + (Math.random() * mainCanvas.width / 2),
      y: 100 + (Math.random() * mainCanvas.height / 2),
      speed: 1 + Math.random() * 20,
      image: Math.floor(Math.random() * (images.length))
    });
  }
  loop();
};

var loop = function() {
  draw();
  requestAnimationFrame(loop);
};

// were our offsite images will be stored
var loadedImage = [];
(function preloadImages() {
  var toLoad = ['https://dl.dropboxusercontent.com/s/4e90e48s5vtmfbd/aaa.png',
    'https://dl.dropboxusercontent.com/s/rumlhyme6s5f8pt/ABC.png'
  ];

  for (var i = 0; i < toLoad.length; i++) {
    var img = new Image();
    img.crossOrigin = 'anonymous';
    img.onload = function() {
      loadedImage.push(this);
      if (loadedImage.length === toLoad.length) {
        init();
      }
    };
    img.src = toLoad[i];
  }
})();

Note that the bigger your images to draw will be, the slowest the drawings will be too.

这篇关于我在使用另一个画布/图像(png)中的画布putImageData复制透明像素时遇到了麻烦的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆