Canvas - floodfill 在边缘留下白色像素

Canvas - floodfill leaves white pixels at edges

我正在创建一个绘图应用程序。我已经成功地完成了所有事情。当我用深色绘制图像时,边缘会出现一些白色像素。我试图改变 r + g + b 和 alpha 的值,但没有用。谁能帮我吗?您可以从 here 查看实时站点。谁愿意帮助我,我就赏金50给him/her。谢谢。这是我的代码。

<script type="text/javascript">
    var initial = screen.width - 50;

    if (initial < 1000) {
        initial = 1000;
    }

    var firsttime = null;

    var colorYellow = {
        r: 255,
        g: 207,
        b: 51
    };

    var context;
    var canvasWidth = initial;
    var canvasHeight = initial;
    var myColor = colorYellow;
    var curColor = myColor;
    var outlineImage = new Image();
    var backgroundImage = new Image();
    var drawingAreaX = 0;
    var drawingAreaY = 0;
    var drawingAreaWidth = initial;
    var drawingAreaHeight = initial;
    var colorLayerData;
    var outlineLayerData;
    var totalLoadResources = 2;
    var curLoadResNum = 0;
    var undoarr = new Array();
    var redoarr = new Array();

    function history(command) { // handles undo/redo button events.
        var data;
        if (command === "redo") {
            data = historyManager.redo(); // get data for redo
        } else
        if (command === "undo") {
            data = historyManager.undo(); // get data for undo
        }
        if (data !== undefined) { // if data has been found
            setColorLayer(data); // set the data
        }
    }

    // sets colour layer and creates copy into colorLayerData
    function setColorLayer(data) {
        context.putImageData(data, 0, 0);
        colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
        context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);
        context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);
    }

    // Clears the canvas.
    function clearCanvas() {
        context.clearRect(0, 0, context.canvas.width, context.canvas.height);
    }



    // Draw the elements on the canvas
    function redraw() {
        uc = 0;
        rc = 0;
        var locX,
            locY;

        // Make sure required resources are loaded before redrawing
        if (curLoadResNum < totalLoadResources) {
            return; // To check if images are loaded successfully or not.
        }

        clearCanvas();
        // Draw the current state of the color layer to the canvas
        context.putImageData(colorLayerData, 0, 0);

        historyManager.push(context.getImageData(0, 0, canvasWidth, canvasHeight));
        redoarr = new Array();
        // Draw the background
        context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);

        // Draw the outline image on top of everything. We could move this to a separate 
        //   canvas so we did not have to redraw this everyime.
        context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);
    };

    function matchOutlineColor(r, g, b, a) {

        return (r + g + b < 50 && a >= 50);
    };

    function matchStartColor(pixelPos, startR, startG, startB) {

        var r = outlineLayerData.data[pixelPos],
            g = outlineLayerData.data[pixelPos + 1],
            b = outlineLayerData.data[pixelPos + 2],
            a = outlineLayerData.data[pixelPos + 3];

        // If current pixel of the outline image is black
        if (matchOutlineColor(r, g, b, a)) {
            return false;
        }

        r = colorLayerData.data[pixelPos];
        g = colorLayerData.data[pixelPos + 1];
        b = colorLayerData.data[pixelPos + 2];

        // If the current pixel matches the clicked color
        if (r === startR && g === startG && b === startB) {
            return true;
        }

        // If current pixel matches the new color
        if (r === curColor.r && g === curColor.g && b === curColor.b) {
            return false;
        }

        return true;
    };

    function colorPixel(pixelPos, r, g, b, a) {
        colorLayerData.data[pixelPos] = r;
        colorLayerData.data[pixelPos + 1] = g;
        colorLayerData.data[pixelPos + 2] = b;
        colorLayerData.data[pixelPos + 3] = a !== undefined ? a : 255;
    };

    function floodFill(startX, startY, startR, startG, startB) {

        document.getElementById('color-lib-1').style.display = "none";
        document.getElementById('color-lib-2').style.display = "none";
        document.getElementById('color-lib-3').style.display = "none";
        document.getElementById('color-lib-4').style.display = "none";
        document.getElementById('color-lib-5').style.display = "none";

        change = false;

        var newPos,
            x,
            y,
            pixelPos,
            reachLeft,
            reachRight,
            drawingBoundLeft = drawingAreaX,
            drawingBoundTop = drawingAreaY,
            drawingBoundRight = drawingAreaX + drawingAreaWidth - 1,
            drawingBoundBottom = drawingAreaY + drawingAreaHeight - 1,
            pixelStack = [
                [startX, startY]
            ];

        while (pixelStack.length) {

            newPos = pixelStack.pop();
            x = newPos[0];
            y = newPos[1];

            // Get current pixel position
            pixelPos = (y * canvasWidth + x) * 4;

            // Go up as long as the color matches and are inside the canvas
            while (y >= drawingBoundTop && matchStartColor(pixelPos, startR, startG, startB)) {
                y -= 1;
                pixelPos -= canvasWidth * 4;
            }

            pixelPos += canvasWidth * 4;
            y += 1;
            reachLeft = false;
            reachRight = false;

            // Go down as long as the color matches and in inside the canvas
            while (y <= drawingBoundBottom && matchStartColor(pixelPos, startR, startG, startB)) {
                y += 1;

                colorPixel(pixelPos, curColor.r, curColor.g, curColor.b);

                if (x > drawingBoundLeft) {
                    if (matchStartColor(pixelPos - 4, startR, startG, startB)) {
                        if (!reachLeft) {
                            // Add pixel to stack
                            pixelStack.push([x - 1, y]);
                            reachLeft = true;
                        }

                    } else if (reachLeft) {
                        reachLeft = false;
                    }
                }

                if (x < drawingBoundRight) {
                    if (matchStartColor(pixelPos + 4, startR, startG, startB)) {
                        if (!reachRight) {
                            // Add pixel to stack
                            pixelStack.push([x + 1, y]);
                            reachRight = true;
                        }
                    } else if (reachRight) {
                        reachRight = false;
                    }
                }

                pixelPos += canvasWidth * 4;
            }
        }
    };

    // Start painting with paint bucket tool starting from pixel specified by startX and startY
    function paintAt(startX, startY) {

        var pixelPos = (startY * canvasWidth + startX) * 4,
            r = colorLayerData.data[pixelPos],
            g = colorLayerData.data[pixelPos + 1],
            b = colorLayerData.data[pixelPos + 2],
            a = colorLayerData.data[pixelPos + 3];

        if (r === curColor.r && g === curColor.g && b === curColor.b) {
            // Return because trying to fill with the same color
            return;
        }

        if (matchOutlineColor(r, g, b, a)) {
            // Return because clicked outline
            return;
        }

        floodFill(startX, startY, r, g, b);

        redraw();
    };

    // Add mouse event listeners to the canvas
    function createMouseEvents() {

        $('#canvas').mousedown(function(e) {

            // Mouse down location
            var mouseX = e.pageX - this.offsetLeft,
                mouseY = e.pageY - this.offsetTop;

            // assuming that the mouseX and mouseY are the mouse coords.
            if (this.style.width) { // make sure there is a width in the style 
                // (assumes if width is there then height will be too
                var w = Number(this.style.width.replace("px", "")); // warning this will not work if size is not in pixels
                var h = Number(this.style.height.replace("px", "")); // convert the height to a number
                var pixelW = this.width; // get  the canvas resolution
                var pixelH = this.height;
                mouseX = Math.floor((mouseX / w) * pixelW); // convert the mouse coords to pixel coords
                mouseY = Math.floor((mouseY / h) * pixelH);
            }

            if ((mouseY > drawingAreaY && mouseY < drawingAreaY + drawingAreaHeight) && (mouseX <= drawingAreaX + drawingAreaWidth)) {
                paintAt(mouseX, mouseY);
            }
        });
    };

    resourceLoaded = function() {

        curLoadResNum += 1;
        //if (curLoadResNum === totalLoadResources) {
        createMouseEvents();
        redraw();
        //}
    };

    var historyManager = (function() { // Anon for private (closure) scope
        var uBuffer = []; // this is undo buff
        var rBuffer = []; // this is redo buff
        var currentState = undefined; // this holds the current history state
        var undoElement = undefined;
        var redoElement = undefined;
        var manager = {
            UI: { // UI interface just for disable and enabling redo undo buttons
                assignUndoButton: function(element) {
                    undoElement = element;
                    this.update();
                },
                assignRedoButton: function(element) {
                    redoElement = element;
                    this.update();
                },
                update: function() {
                    if (redoElement !== undefined) {
                        redoElement.disabled = (rBuffer.length === 0);
                    }
                    if (undoElement !== undefined) {
                        undoElement.disabled = (uBuffer.length === 0);
                    }
                }
            },
            reset: function() {
                uBuffer.length = 0;
                rBuffer.length = 0;
                currentState = undefined;
                this.UI.update();
            },
            push: function(data) {
                if (currentState !== undefined) {
                    var same = true
                    for (i = 0; i < data.data.length; i++) {
                        if (data.data[i] !== currentState.data[i]) {
                            same = false;
                            break;
                        }
                    }
                    if (same) {
                        return;
                    }
                }
                if (currentState !== undefined) {
                    uBuffer.push(currentState);
                }
                currentState = data;
                rBuffer.length = 0;
                this.UI.update();
            },
            undo: function() {
                if (uBuffer.length > 0) {
                    if (currentState !== undefined) {
                        rBuffer.push(currentState);
                    }
                    currentState = uBuffer.pop();
                }
                this.UI.update();
                return currentState; // return data or unfefined
            },
            redo: function() {
                if (rBuffer.length > 0) {
                    if (currentState !== undefined) {
                        uBuffer.push(currentState);
                    }
                    currentState = rBuffer.pop();
                }
                this.UI.update();
                return currentState;
            },
        }
        return manager;
    })();

    function start() {

        var canvas = document.createElement('canvas');
        canvas.setAttribute('width', canvasWidth);
        canvas.setAttribute('height', canvasHeight);
        canvas.setAttribute('id', 'canvas');
        document.getElementById('canvasDiv').appendChild(canvas);

        if (typeof G_vmlCanvasManager !== "undefined") {
            canvas = G_vmlCanvasManager.initElement(canvas);
        }
        context = canvas.getContext("2d");
        backgroundImage.onload = resourceLoaded();
        backgroundImage.src = "images/t.png";

        outlineImage.onload = function() {
            context.drawImage(outlineImage, drawingAreaX, drawingAreaY, drawingAreaWidth, drawingAreaHeight);

            try {
                outlineLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
            } catch (ex) {
                window.alert("Application cannot be run locally. Please run on a server.");
                return;
            }
            clearCanvas();
            colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
            resourceLoaded();
        };
        outlineImage.src = "images/products/<?php echo $product['product_image']; ?>";
    };

    if (historyManager !== undefined) {
        // only for visual feedback and not required for the history manager to function.
        historyManager.UI.assignUndoButton(document.querySelector("#undo-button"));
        historyManager.UI.assignRedoButton(document.querySelector("#redo-button"));
    }

    getColor = function() {

    };
</script>

是的,遗漏的 "white" 点实际上不是白色的,因为白色和黑色之间存在微小的渐变。尝试在这些行周围给它一些余地:

 if ((r <= curColor.r + 10 && r >= curColor.r - 10)  && (r >= curColor.g - 10 && r <= curColor.g + 10) && (b >= curColor.b - 10 && b <= curColor.b + 10)) {
        return false;
    }

您可以修改 10 因子,直到它看起来不错。只是调整它,直到它没问题。 (可能是错误的代码,我刚醒来,但你应该得到照片 :D )

您还可以在单​​独的缓冲区中预处理图像并减少颜色数。这样更容易填充渐变的开头,从而减少或消除您描述的不良效果。

老实说,与其说是您的绘图程序的问题,不如说是所绘制图像的问题。 'white' 像素实际上是浅灰色,这是用于在图像中绘制线条的画笔工具的副作用。有两种解决方法:

  1. 从图像中去除所有浅灰色像素并使它们变白。使用颜色 select 工具和铅笔工具可以解决这个问题。唯一的副作用是某些点的线条可能看起来有点生涩。

  2. 在涉及到哪些颜色被覆盖时给予一些宽大处理。因此,不仅要替换纯白色,还要替换浅灰色。 #CCC(或 rgb(204, 204, 204))附近的任何颜色都应该涂上颜色。

方案二的代码如下:

if(r >= 204 && g >= 204 && b >= 204 && r === g && g === b){
    return true;
}

这只是检查像素是否为浅灰度颜色,如果是,则 returns 为真。使用它代替您的轮廓颜色检查功能。

我建议进行两项更改:

  1. 混合像素和填充颜色而不是硬覆盖
  2. 根据强度梯度变化而不是简单的阈值来限制填充区域

在水平和垂直方向上填充,直到强度梯度的符号从 + 翻转到 - 或 - 到 + 让我们填充整个区域,包括黑色边框的 'half'。通过检查梯度,我们只是确保不超过强度最小值,从而避免填充相邻区域。

看看下面的演示:

// Get pixel intensity:
function getIntensity(data, i) {
  return data[i] + data[i + 1] + data[i + 2];
}

// Set pixel color:
function setColor(data, i, r, g, b) {
  data[i] &= r;
  data[i + 1] &= g;
  data[i + 2] &= b;
}

// Fill a horizontal line:
function fill(x, y, data, width, r, g, b) {
  var i_start = y * (width << 2);
  var i = i_start + (x << 2);
  var i_end = i_start + (width << 2);
  var i_intensity = getIntensity(data, i);

  // Horizontal line to the right:
  var prev_gradient = 0;
  var prev_intensity = i_intensity;
  for (var j = i; j < i_end; j += 4) {
    var intensity = getIntensity(data, j);
    gradient = intensity - prev_intensity;
    prev_intensity = intensity;
    if ((prev_gradient > 0 && gradient < 0) || (prev_gradient < 0 && gradient > 0)) break;
    if (gradient != 0) prev_gradient = gradient;

    setColor(data, j, 255, 0, 0);
  }

  // Horizontal line to the left:
  prev_gradient = 0;
  prev_intensity = i_intensity;
  for (var j = i - 4; j > i_start; j -= 4) {
    var intensity = getIntensity(data, j);
    gradient = intensity - prev_intensity;
    prev_intensity = intensity;
    if ((prev_gradient > 0 && gradient < 0) || (prev_gradient < 0 && gradient > 0)) break;
    if (gradient != 0) prev_gradient = gradient;

    setColor(data, j, 255, 0, 0);
  }
}

// Demo canvas:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');

// Fill horizontal line on click:
canvas.addEventListener('mousedown', event => {
  var rect = canvas.getBoundingClientRect();
  var x = event.clientX - rect.left | 0;
  var y = event.clientY - rect.top | 0;

  var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
  fill(x, y, imageData.data, imageData.width);
  context.putImageData(imageData, 0, 0);
});

// Load a sample image:
var image = new Image();
image.addEventListener('load', event => {
  context.drawImage(event.target, 0, 0, canvas.width, canvas.height);
});
image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAACCAIAAAABwbG5AAAAA3NCSVQICAjb4U/gAAAAGXRFWHRTb2Z0d2FyZQBnbm9tZS1zY3JlZW5zaG907wO/PgAAACZJREFUCJlj+I8Kbt68mZeXd/PmTbgIw38MsG/fvoKCgqdPn0K4ACAfPGrloJp1AAAAAElFTkSuQmCC';
<canvas id="canvas"></canvas>

您可以通过仅跟踪一个颜色通道而不是对 getIntensity 函数的所有三个颜色通道求和来提高性能。

此外,由于混合模式的原因,您需要处理 'clearing' 一个已经填充的区域,然后再用另一种颜色填充它。

你可以。 G。在内存中保留背景图像的单通道灰度图像数据数组,并将其用作填充算法的源图像。

(手动或自动)将所有灰度图像转换为具有二进制边界的 1 位轮廓并将其用作填充算法的源,将结果与灰度图像平滑地混合可能会更高效。

您对轮廓的检查太严格了,将浅灰色像素标记为您无法着色的像素。我只是调整了您的阈值:

function matchOutlineColor (r,g,b,a,e) {
  return a >= 150;
}

请注意我的白色值和 alpha 值要高得多。您也许可以进一步调整,但这对我来说看起来不错。以下是一些前后照片:

之前(放大 200%)

之后(放大 200%)