Reputation: 1670
I am using sketch.js to dynamically draw very simple shapes on HTML5 canvas.
Is there a way to calculate the number of pixels inside a closed shape drawn on the canvas?
Upvotes: 1
Views: 3515
Reputation: 105035
Here's how you count non-transparent pixels on a canvas
// get a reference to your canvas
var c=document.getElementById("canvas");
var ctx=c.getContext("2d");
// get the pixel data from the canvas
var imgData=ctx.getImageData(0,0,c.width,c.height);
// loop through each pixel and count non-transparent pixels
var count=0;
for (var i=0;i<imgData.data.length;i+=4)
{
// nontransparent = imgData.data[i+3]==0
if(imgData.data[i+3]==0){ count++; }
}
[Edited: to get pixel count of "if-filled" closed shapes on a canvas]
I usually use this code to do masking in canvas, but I’ve adapted it here to get your pixel count inside your closed shape.
A few caviats:
Because of the “neighboring” algorithm being used, the strokewidth must be at least 2 pixels wide for uncurved shapes and at least 3 pixels wide for shapes containing curves.
Because Canvas automatically draws strokes with anti-aliasing, the inside pixel count will always be slightly larger than expected. This is because anti-aliasing “eats into” the stroke, effectively causing more inside pixels than expected. BTW, there is no way to turn off canvas’s anti-aliasing, if you try getImageData(), setting all shape pixels to rbg(0,0,0), putImageData(), the resulting image will also be anti-aliased—just more jagged!
Here is the code:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
canvas{border:1px solid red;}
</style>
<script>
$(function(){
// The floodFill algorithm below is based on the good work by William Malone, Copyright 2010 William Malone (www.williammalone.com) -- Apache License: http://www.apache.org/licenses/LICENSE-2.0 -- Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
var canvas=document.getElementById("canvas");
var context = canvas.getContext("2d");
var canvasWidth = canvas.width;
var canvasHeight = canvas.height;
var strokeColor = {r: 0, g: 0, b: 0};
var fillColor = {r: 101,g: 155,b: 65};
var fillData;
var strokeData;
function redraw() {
context.clearRect(0, 0, canvasWidth, canvasHeight);
context.putImageData(fillData, 0, 0);
drawOutline(context);
}
function matchstrokeColor(r, g, b, a) {
// must check for near black because of anti-aliasing
return (r + g + b < 100 && a === 255);
}
function matchStartColor(pixelPos, startR, startG, startB) {
var r = strokeData.data[pixelPos],
g = strokeData.data[pixelPos + 1],
b = strokeData.data[pixelPos + 2],
a = strokeData.data[pixelPos + 3];
// If current pixel of the outline image is black-ish
if (matchstrokeColor(r, g, b, a)) {
return false;
}
r = fillData.data[pixelPos];
g = fillData.data[pixelPos + 1];
b = fillData.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 === fillColor.r && g === fillColor.g && b === fillColor.b) {
return false;
}
return true;
}
function setPixel(pixelPos, r, g, b, a) {
fillData.data[pixelPos] = r;
fillData.data[pixelPos + 1] = g;
fillData.data[pixelPos + 2] = b;
fillData.data[pixelPos + 3] = a !== undefined ? a : 255;
}
function floodFill(startX, startY, startR, startG, startB) {
var newPos;
var x;
var y;
var pixelPos;
var neighborLeft;
var neighborRight;
var 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 >= 0 && matchStartColor(pixelPos, startR, startG, startB)) {
y -= 1;
pixelPos -= canvasWidth * 4;
}
pixelPos += canvasWidth * 4;
y += 1;
neighborLeft = false;
neighborRight = false;
// Go down as long as the color matches and in inside the canvas
while (y <= (canvasHeight-1) && matchStartColor(pixelPos, startR, startG, startB)) {
y += 1;
setPixel(pixelPos, fillColor.r, fillColor.g, fillColor.b);
if (x > 0) {
if (matchStartColor(pixelPos - 4, startR, startG, startB)) {
if (!neighborLeft) {
// Add pixel to stack
pixelStack.push([x - 1, y]);
neighborLeft = true;
}
} else if (neighborLeft) {
neighborLeft = false;
}
}
if (x < (canvasWidth-1)) {
if (matchStartColor(pixelPos + 4, startR, startG, startB)) {
if (!neighborRight) {
// Add pixel to stack
pixelStack.push([x + 1, y]);
neighborRight = true;
}
} else if (neighborRight) {
neighborRight = false;
}
}
pixelPos += canvasWidth * 4;
}
}
}
// Fill
function paintAt(startX, startY) {
var pixelPos = (startY * canvasWidth + startX) * 4,
r = fillData.data[pixelPos],
g = fillData.data[pixelPos + 1],
b = fillData.data[pixelPos + 2],
a = fillData.data[pixelPos + 3];
if (r === fillColor.r && g === fillColor.g && b === fillColor.b) {
// this one's already filled
return;
}
if (matchstrokeColor(r, g, b, a)) {
return;
}
floodFill(startX, startY, r, g, b);
redraw();
}
function init() {
var theShapes=document.getElementById("theShapes");
var theShapesContext=theShapes.getContext("2d");
drawOutline(theShapesContext);
drawOutline(context);
strokeData = context.getImageData(0, 0, canvasWidth, canvasHeight);
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
fillData = context.getImageData(0, 0, canvasWidth, canvasHeight);
$('#canvas').mousedown(function (e) {
// Mouse down location
var mouseX = e.pageX - this.offsetLeft;
var mouseY = e.pageY - this.offsetTop;
paintAt(mouseX, mouseY);
});
redraw();
};
function drawOutline(theContext){
theContext.beginPath();
theContext.moveTo(55, 60);
theContext.bezierCurveTo(35, 70, 35, 95, 85, 95);
theContext.bezierCurveTo( 95,110, 130,110, 140, 95);
theContext.bezierCurveTo(180, 95, 180, 80, 165, 70);
theContext.bezierCurveTo(185, 40, 155, 35, 140, 45);
theContext.bezierCurveTo(130, 25, 95, 30, 95, 45);
theContext.bezierCurveTo( 70, 25, 45, 30, 55, 60);
theContext.closePath();
theContext.rect(200,30,100,70);
theContext.lineWidth = 3;
theContext.strokeStyle = 'rgb(0,0,0)';
theContext.stroke();
}
function getFilledPixelCount(theContext,theCanvas){
// get the pixel data from the fill canvas
var imgData=theContext.getImageData(0,0,theCanvas.width,theCanvas.height);
console.log(imgData.data.length);
var count=0;
for (var i=0;i<imgData.data.length;i+=4){
r = imgData.data[i],
g = imgData.data[i + 1],
b = imgData.data[i + 2],
a = imgData.data[i + 3];
if (r === fillColor.r && g === fillColor.g && b === fillColor.b) {
count++;
}
}
return(count);
}
$("#counter").click(function(){alert("There are "+getFilledPixelCount(context,canvas)+" filled pixels.");});
init();
}); // end $(function(){});
</script>
</head>
<body>
<p>The original stroked shapes</p>
<canvas id="theShapes" width=350 height=150></canvas><br/>
<p>The filled shapes used for pixel counting</p>
<p>Click inside a shape below</p>
<canvas id="canvas" width=350 height=150></canvas><br/>
<button id="counter">Filled Count</button>
</body>
</html>
Upvotes: 5