简体   繁体   English

画布洪水填充未填充到边缘

[英]Canvas flood fill not filling to edge

I am using a flood fill algorithm to fill in circles drawn on the canvas. 我正在使用泛洪填充算法来填充在画布上绘制的圆圈。 The issue I am having is that the algorithm isn't filling right up to the edge of the circle. 我遇到的问题是该算法无法完全填充到圆的边缘。

Here is the algorithm based on this blog post : 这是基于此博客文章的算法:

function paintLocation(startX, startY, r, g, b) {
    var colorLayer = context1.getImageData(0, 0, canvasWidth, canvasHeight);
    pixelPos = (startY * canvasWidth + startX) * 4;

    startR = colorLayer.data[pixelPos];
    startG = colorLayer.data[pixelPos + 1];
    startB = colorLayer.data[pixelPos + 2];

    var pixelStack = [
        [startX, startY]
    ];

    var drawingBoundTop = 0;
    while (pixelStack.length) {
        var newPos, x, y, pixelPos, reachLeft, reachRight;
        newPos = pixelStack.pop();
        x = newPos[0];
        y = newPos[1];

        pixelPos = (y * canvasWidth + x) * 4;
        while (y-- >= drawingBoundTop && matchStartColor(colorLayer, pixelPos, startR, startG, startB)) {
            pixelPos -= canvasWidth * 4;
        }
        pixelPos += canvasWidth * 4;
        ++y;
        reachLeft = false;
        reachRight = false;
        while (y++ < canvasHeight - 1 && matchStartColor(colorLayer, pixelPos, startR, startG, startB)) {
            colorPixel(colorLayer, pixelPos, r, g, b);

            if (x > 0) {
                if (matchStartColor(colorLayer, pixelPos - 4, startR, startG, startB)) {
                    if (!reachLeft) {
                        pixelStack.push([x - 1, y]);
                        reachLeft = true;
                    }
                } else if (reachLeft) {
                    reachLeft = false;
                }
            }

            if (x < canvasWidth - 1) {
                if (matchStartColor(colorLayer, pixelPos + 4, startR, startG, startB)) {
                    if (!reachRight) {
                        pixelStack.push([x + 1, y]);
                        reachRight = true;
                    }
                } else if (reachRight) {
                    reachRight = false;
                }
            }

            pixelPos += canvasWidth * 4;
        }
    }
    context1.putImageData(colorLayer, 0, 0);
}

Please see the JSFiddle or the below image to see what I mean. 请查看JSFiddle或下面的图片以了解我的意思。 Clicking inside any circles will change the colour between yellow and black (the issue is far more visible with black). 在任何圆圈内单击都会在黄色和黑色之间更改颜色(黑色的问题更明显)。

I've read that the issue could be something to do with the anti-aliasing and I have tried turning it off with context1.imageSmoothingEnabled = true; 我已经知道问题可能与抗锯齿有关,并且我尝试使用context1.imageSmoothingEnabled = true;将其关闭context1.imageSmoothingEnabled = true; but it didn't make a difference. 但这并没有改变。

洪水填充问题

I have also tried changing my matchStartColour function as per this question but that doesn't help. 我也尝试根据问题更改我的matchStartColour函数,但这无济于事。

function matchStartColor(colorLayer, pixelPos, startR, startG, startB) {
    var r = colorLayer.data[pixelPos];
    var g = colorLayer.data[pixelPos + 1];
    var b = colorLayer.data[pixelPos + 2];

    return (r == startR && g == startG && b == startB);
}

I think it might have something to do with the fact that the circles have no fill colour and the background of the canvas isn't white but it is transparent black. 我认为这可能与以下事实有关:圆圈没有填充颜色,并且画布的背景不是白色,而是透明的黑色。 I have tried changing the canvas background to white but that also didn't help. 我尝试将画布背景更改为白色,但这也无济于事。

Use flood fill to create a Mask 使用泛洪填充创建蒙版

I just happened to do a floodFill the other day that addresses the problem of antialiased edges. 前几天,我刚好要进行泛洪处理,以解决抗锯齿边缘的问题。

Rather than paint to the canvas directly, I paint to a byte array that is then used to create a mask. 我没有直接绘制到画布上,而是绘制了一个字节数组,然后将其用于创建遮罩。 The mask allows for the alpha values to be set. 遮罩允许设置Alpha值。

The fill can have a tolerance and a toleranceFade that control how it deals with colours that approch the tolerance value. 填充可以有一个tolerance和一个toleranceFade淡入toleranceFade ,用于控制如何处理接近公差值的颜色。

When pixel's difference between the start colour and tolerance are greater than (tolerance - toleranceFade) I set the alpha for that pixel to 255 - ((differance - (tolerance - toleranceFade)) / toleranceFade) * 255 which creates a nice smooth blend at the edges of lines. 当像素的起始颜色和公差之间的差异大于(tolerance - toleranceFade)我将该像素的alpha设置为255 - ((differance - (tolerance - toleranceFade)) / toleranceFade) * 255 ,这会在线条的边缘。 Though it does not work for all situations for high contrast situations it is an effective solution. 尽管它不适用于高对比度情况下的所有情况,但它是一种有效的解决方案。

The example below shows the results of with and without the toleranceFade . 下面的示例显示了带有和不带有toleranceFade的结果。 The blue is without the toleranceFade , the red is with the tolerance set at 190 and the toleranceFade of 90. 蓝色不带toleranceFade ,红色不带tolerance设置为190且toleranceFade为90。

You will have to play around with the setting to get the best results for your needs. 您将不得不尝试该设置,以获得满足您需求的最佳结果。

 function showExample(){ var canvas = document.createElement("canvas"); canvas.width = 200; canvas.height = 200; var ctx = canvas.getContext("2d"); document.body.appendChild(canvas); ctx.fillStyle = "white" ctx.fillRect(0,0,canvas.width,canvas.height) ctx.lineWidth = 4; ctx.strokeStyle = "black" ctx.beginPath(); ctx.arc(100,100,90,0,Math.PI * 2); ctx.arc(120,100,60,0,Math.PI * 2); ctx.stroke(); ctx.fillStyle = "blue"; floodFill.fill(100,100,1,ctx) ctx.fillStyle = "red"; floodFill.fill(40,100,190,ctx,null,null,90) } // FloodFill2D from https://github.com/blindman67/FloodFill2D var floodFill = (function(){ "use strict"; const extent = { top : 0, left : 0, bottom : 0, right : 0, } var keepMask = false; // if true then a mask of the filled area is returned as a canvas image var extentOnly = false; // if true then the extent of the fill is returned var copyPixels = false; // if true then creating a copy of filled pixels var cutPixels = false; // if true and copyPixels true then filled pixels are removed var useBoundingColor = false; // Set the colour to fill up to. Will not fill over this colour var useCompareColor = false; // Rather than get the pixel at posX,posY use the compareColours var red, green, blue, alpha; // compare colours if var canvas,ctx; function floodFill (posX, posY, tolerance, context2D, diagonal, area, toleranceFade) { var w, h, painted, x, y, ind, sr, sg, sb, sa,imgData, data, data32, RGBA32, stack, stackPos, lookLeft, lookRight, i, colImgDat, differance, checkColour; toleranceFade = toleranceFade !== undefined && toleranceFade !== null ? toleranceFade : 0; diagonal = diagonal !== undefined && diagonal !== null ? diagonal : false; area = area !== undefined && area !== null ? area : {}; area.x = area.x !== undefined ? area.x : 0; area.y = area.y !== undefined ? area.y : 0; area.w = area.w !== undefined ? area.w : context2D.canvas.width - area.x; area.h = area.h !== undefined ? area.h : context2D.canvas.height - area.y; // vet area is on the canvas. if(area.x < 0){ area.w = area.x + area.w; area.x = 0; } if(area.y < 0){ area.h = area.y + area.h; area.y = 0; } if(area.x >= context2D.canvas.width || area.y >= context2D.canvas.height){ return false; } if(area.x + area.w > context2D.canvas.width){ area.w = context2D.canvas.width - area.x; } if(area.y + area.h > context2D.canvas.height){ area.h = context2D.canvas.height - area.y; } if(area.w <= 0 || area.h <= 0){ return false; } w = area.w; // width and height h = area.h; x = posX - area.x; y = posY - area.y; if(extentOnly){ extent.left = x; // set up extent extent.right = x; extent.top = y; extent.bottom = y; } if(x < 0 || y < 0 || x >= w || y >= h){ return false; // fill start outside area. Don't do anything } if(tolerance === 255 && toleranceFade === 0 && ! keepMask){ // fill all if(extentOnly){ extent.left = area.x; // set up extent extent.right = area.x + w; extent.top = area.y; extent.bottom = area.y + h; } context2D.fillRect(area.x,area.y,w,h); return true; } if(toleranceFade > 0){ // add one if on to get correct number of steps toleranceFade += 1; } imgData = context2D.getImageData(area.x,area.y,area.w,area.h); data = imgData.data; // image data to fill; data32 = new Uint32Array(data.buffer); painted = new Uint8ClampedArray(w*h); // byte array to mark painted area; function checkColourAll(ind){ if( ind < 0 || painted[ind] > 0){ // test bounds return false; } var ind4 = ind << 2; // get index of pixel if((differance = Math.max( // get the max channel difference; Math.abs(sr - data[ind4++]), Math.abs(sg - data[ind4++]), Math.abs(sb - data[ind4++]), Math.abs(sa - data[ind4++]) )) > tolerance){ return false; } return true } // check to bounding colour function checkColourBound(ind){ if( ind < 0 || painted[ind] > 0){ // test bounds return false; } var ind4 = ind << 2; // get index of pixel differance = 0; if(sr === data[ind4] && sg === data[ind4 + 1] && sb === data[ind4 + 2] && sa === data[ind4 + 3]){ return false } return true } // this function checks the colour of only selected channels function checkColourLimited(ind){ // check only colour channels that are not null var dr,dg,db,da; if( ind < 0 || painted[ind] > 0){ // test bounds return false; } var ind4 = ind << 2; // get index of pixel dr = dg = db = da = 0; if(sr !== null && (dr = Math.abs(sr - data[ind4])) > tolerance){ return false; } if(sg !== null && (dg = Math.abs(sg - data[ind4 + 1])) > tolerance){ return false; } if(sb !== null && (db = Math.abs(sb - data[ind4 + 2])) > tolerance){ return false; } if(sa !== null && (da = Math.abs(sa - data[ind4 + 3])) > tolerance){ return false; } diferance = Math.max(dr, dg, db, da); return true } // set which function to check colour with checkColour = checkColourAll; if(useBoundingColor){ sr = red; sg = green; sb = blue; if(alpha === null){ ind = (y * w + x) << 2; // get the starting pixel index sa = data[ind + 3]; }else{ sa = alpha; } checkColour = checkColourBound; useBoundingColor = false; }else if(useCompareColor){ sr = red; sg = green; sb = blue; sa = alpha; if(red === null || blue === null || green === null || alpha === null){ checkColour = checkColourLimited; } useCompareColor = false; }else{ ind = (y * w + x) << 2; // get the starting pixel index sr = data[ind]; // get the start colour that we will use tolerance against. sg = data[ind + 1]; sb = data[ind + 2]; sa = data[ind + 3]; } stack = []; // paint stack to find new pixels to paint lookLeft = false; // test directions lookRight = false; stackPos = 0; stack[stackPos++] = x; stack[stackPos++] = y; while (stackPos > 0) { // do while pixels on the stack y = stack[--stackPos]; // get the pixel y x = stack[--stackPos]; // get the pixel x ind = x + y * w; while (checkColour(ind - w)) { // find the top most pixel within tollerance; y -= 1; ind -= w; } //checkTop left and right if allowing diagonal painting if(diagonal && y > 0){ if(x > 0 && !checkColour(ind - 1) && checkColour(ind - w - 1)){ stack[stackPos++] = x - 1; stack[stackPos++] = y - 1; } if(x < w - 1 && !checkColour(ind + 1) && checkColour(ind - w + 1)){ stack[stackPos++] = x + 1; stack[stackPos++] = y - 1; } } lookLeft = false; // set look directions lookRight = false; // only look is a pixel left or right was blocked while (checkColour(ind) && y < h) { // move down till no more room if(toleranceFade > 0 && differance >= tolerance-toleranceFade){ painted[ind] = 255 - (((differance - (tolerance - toleranceFade)) / toleranceFade) * 255); painted[ind] = painted[ind] === 0 ? 1 : painted[ind]; // min value must be 1 }else{ painted[ind] = 255; } if(extentOnly){ extent.left = x < extent.left ? x : extent.left; // Faster than using Math.min extent.right = x > extent.right ? x : extent.right; // Faster than using Math.min extent.top = y < extent.top ? y : extent.top; // Faster than using Math.max extent.bottom = y > extent.bottom ? y : extent.bottom; // Faster than using Math.max } if (checkColour(ind - 1) && x > 0) { // check left is blocked if (!lookLeft) { stack[stackPos++] = x - 1; stack[stackPos++] = y; lookLeft = true; } } else if (lookLeft) { lookLeft = false; } if (checkColour(ind + 1) && x < w -1) { // check right is blocked if (!lookRight) { stack[stackPos++] = x + 1; stack[stackPos++] = y; lookRight = true; } } else if (lookRight) { lookRight = false; } y += 1; // move down one pixel ind += w; } if(diagonal && y < h){ // check for diagonal areas and push them to be painted if(checkColour(ind - 1) && !lookLeft && x > 0){ stack[stackPos++] = x - 1; stack[stackPos++] = y; } if(checkColour(ind + 1) && !lookRight && x < w - 1){ stack[stackPos++] = x + 1; stack[stackPos++] = y; } } } if(extentOnly){ extent.top += area.y; extent.bottom += area.y; extent.left += area.x; extent.right += area.x; return true; } canvas = document.createElement("canvas"); canvas.width = w; canvas.height = h; ctx = canvas.getContext("2d"); ctx.fillStyle = context2D.fillStyle; ctx.fillRect(0, 0, w, h); colImgDat = ctx.getImageData(0, 0, w, h); if(copyPixels){ i = 0; ind = 0; if(cutPixels){ while(i < painted.length){ if(painted[i] > 0){ colImgDat.data[ind] = data[ind]; colImgDat.data[ind + 1] = data[ind + 1]; colImgDat.data[ind + 2] = data[ind + 2]; colImgDat.data[ind + 3] = data[ind + 3] * (painted[i] / 255); data[ind + 3] = 255 - painted[i]; }else{ colImgDat.data[ind + 3] = 0; } i ++; ind += 4; } context2D.putImageData(imgData, area.x, area.y); }else{ while(i < painted.length){ if(painted[i] > 0){ colImgDat.data[ind] = data[ind]; colImgDat.data[ind + 1] = data[ind + 1]; colImgDat.data[ind + 2] = data[ind + 2]; colImgDat.data[ind + 3] = data[ind + 3] * (painted[i] / 255); }else{ colImgDat.data[ind + 3] = 0; } i ++; ind += 4; } } ctx.putImageData(colImgDat,0,0); return true; }else{ i = 0; ind = 3; while(i < painted.length){ colImgDat.data[ind] = painted[i]; i ++; ind += 4; } ctx.putImageData(colImgDat,0,0); } if(! keepMask){ context2D.drawImage(canvas,area.x,area.y,w,h); } return true; } return { fill : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){ floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade); ctx = undefined; canvas = undefined; }, getMask : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){ keepMask = true; floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade); ctx = undefined; keepMask = false; return canvas; }, getExtent : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){ extentOnly = true; if(floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade)){ extentOnly = false; return { top : extent.top, left : extent.left, right : extent.right, bottom : extent.bottom, width : extent.right - extent.left, height : extent.bottom - extent.top, } } extentOnly = false; return null; }, cut : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){ cutPixels = true; copyPixels = true; floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade); cutPixels = false; copyPixels = false; ctx = undefined; return canvas; }, copy : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){ cutPixels = false; copyPixels = true; floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade); copyPixels = false; ctx = undefined; return canvas; }, setCompareValues : function(R,G,B,A){ if(R === null && G === null && B === null && A === null){ return; } red = R; green = G; blue = B; alpha = A; useBoundingColor = false; useCompareColor = true; }, setBoundingColor : function(R,G,B,A){ red = R; green = G; blue = B; alpha = A; useCompareColor = false; useBoundingColor = true; } } }()); showExample(); 
 Red floodFill.fill(40,100,190,ctx,null,null,90) tolerance 190, tolerance fade 90<br>Blue floodFill.fill(100,100,1,ctx) tolerance 1.<br> 

For more info see readme at Github FloodFill2D 有关更多信息,请参见Github FloodFill2D上的自述文件。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM