Вопрос

I'm looking for a method of detecting a shape in a transparent PNG. For example, I will create a transparent canvas of 940x680, then place a fully opaque object somewhere in that canvas.

I want to be able to detect the size (w, h), and top + left location of that object.

Here is an example of the original image:

Transparent PNG Canvas with Image Object

Here is an example of what I would like to achieve (Bounding box overlay, with top + left margin data):

Results Image

I've found a resource that does some transparency detection, but I'm not sure how I scale something like this to what I'm looking for.

var imgData,
    width = 200,
    height = 200;

$('#mask').bind('mousemove', function(ev){
    if(!imgData){ initCanvas(); }
    var imgPos = $(this).offset(),
      mousePos = {x : ev.pageX - imgPos.left, y : ev.pageY - imgPos.top},
      pixelPos = 4*(mousePos.x + height*mousePos.y),
         alpha = imgData.data[pixelPos+3];

    $('#opacity').text('Opacity = ' + ((100*alpha/255) << 0) + '%');
});

function initCanvas(){
    var canvas = $('<canvas width="'+width+'" height="'+height+'" />')[0],
           ctx = canvas.getContext('2d');

    ctx.drawImage($('#mask')[0], 0, 0);
    imgData = ctx.getImageData(0, 0, width, height);
}

Fiddle

Это было полезно?

Решение

What you need to do:

  • Get the buffer
  • Get a 32-bits reference of that buffer (If your other pixels are transparent then you can use a Uint32Array buffer to iterate).
  • Scan 0 - width to find x1 edge
  • Scan width - 0 to find x2 edge
  • Scan 0 - height to find y1 edge
  • Scan height - 0 to find y2 edge

These scans can be combined but for simplicity I'll show each step separately.

Online demo of this can be found here.

Result:

Snapshot

When image is loaded draw it in (if the image is small then the rest of this example would be waste as you would know the coordinates when drawing it - assuming here the image you draw is large with a small image inside it)

(note: this is a non-optimized version for the sake of simplicity)

ctx.drawImage(this, 0, 0, w, h);

var idata = ctx.getImageData(0, 0, w, h),      // get image data for canvas
    buffer = idata.data,                       // get buffer (unnes. step)
    buffer32 = new Uint32Array(buffer.buffer), // get a 32-bit representation
    x, y,                                      // iterators
    x1 = w, y1 = h, x2 = 0, y2 = 0;            // min/max values

Then scan each edge. For left edge you scan from 0 to width for each line (non optimized):

// get left edge
for(y = 0; y < h; y++) {                       // line by line
    for(x = 0; x < w; x++) {                   // 0 to width
        if (buffer32[x + y * w] > 0) {         // non-transparent pixel?
            if (x < x1) x1 = x;                // if less than current min update
        }
    }
}

For the right edge you just reverse x iterator:

// get right edge
for(y = 0; y < h; y++) {                       // line by line
    for(x = w; x >= 0; x--) {                  // from width to 0
        if (buffer32[x + y * w] > 0) {
            if (x > x2) x2 = x;
        }
    }
}

And the same is for top and bottom edges just that the iterators are reversed:

// get top edge
for(x = 0; x < w; x++) {
    for(y = 0; y < h; y++) {
        if (buffer32[x + y * w] > 0) {
            if (y < y1) y1 = y;
        }
    }
}

// get bottom edge
for(x = 0; x < w; x++) {
    for(y = h; y >= 0; y--) {
        if (buffer32[x + y * w] > 0) {
            if (y > y2) y2 = y;
        }
    }
}

The resulting region is then:

ctx.strokeRect(x1, y1, x2-x1, y2-y1);

There are various optimizations you could implement but they depend entirely on the scenario such as if you know approximate placement then you don't have to iterate all lines/columns.

You could do a brute force guess of he placement by skipping x number of pixels and when you found a non-transparent pixel you could make a max search area based on that and so forth, but that is out of scope here.

Hope this helps!

Другие советы

I was in need of something similar to this, just recently. Although the question is answered, I wanted to post my code for a future reference.

In my case, I'm drawing a (font) icon on a blank/transparent canvas, and want to get the bounding box. Even if I know the height of the icon (using font-size, i.e., height), I can't know the width. So I have to calculate it manually.

I'm not sure if there's a clever way to calculate this. First thing that popped into my head was doing it the hard way: manually checking every pixel, and that's what I did.

I think the code is pretty self-explanatory, so I won't do any explanation. I tried to keep the code as clean as possible.

/* Layer 3: The App */

let canvas = document.querySelector("#canvas");
let input  = document.querySelector("#input");
let output = document.querySelector("#output");

canvas.width  = 256;
canvas.height = 256;

let context = canvas.getContext("2d");
context.font = "200px Arial, sans-serif";

let drawnLetter = null;

drawLetter(input.value);

function drawLetter(letter) {
    letter = letter ? letter[0] : null;

    if (!letter) {
        // clear canvas
        context.clearRect(0, 0, canvas.width, canvas.height);
        output.textContent = null;
        return;
    }

    if (letter == drawnLetter) {
        return;
    }

    drawnLetter = letter;

    // clear canvas
    context.clearRect(0, 0, canvas.width, canvas.height);

    // draw letter
    context.fillText(letter, 50, canvas.height - 50);

    // find edges
    let boundingBox = findEdges(context);

    // mark the edges
    context.beginPath();
    context.rect(boundingBox.left, boundingBox.top, boundingBox.width, boundingBox.height);
    context.lineWidth = 2;
    context.strokeStyle = "red";
    context.stroke();

    // output the values
    output.textContent = JSON.stringify(boundingBox, null, "  ");
}


/* Layer 2: Interacting with canvas */

function findEdges(context) {
    let left   = findLeftEdge(context);
    let right  = findRightEdge(context);
    let top    = findTopEdge(context);
    let bottom = findBottomEdge(context);
    // right and bottom are relative to top left (0,0)
    return {
        left,
        top,
        right,
        bottom,
        width  : right - left,
        height : bottom - top,
    };
}

function findLeftEdge(context) {
    let imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
    let emptyPixel = [0, 0, 0, 0].join();
    for (let x = 0; x < context.canvas.width; x++) {
        for (let y = 0; y < context.canvas.height; y++) {
            let pixel = getPixel(imageData, x, y).join();
            if (pixel != emptyPixel) {
                return x;
            }
        }
    }
}

function findRightEdge(context) {
    let imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
    let emptyPixel = [0, 0, 0, 0].join();
    for (let x = context.canvas.width - 1; x >= 0; x--) {
        for (let y = 0; y < context.canvas.height; y++) {
            let pixel = getPixel(imageData, x, y).join();
            if (pixel != emptyPixel) {
                return x;
            }
        }
    }
}

function findTopEdge(context) {
    let imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
    let emptyPixel = [0, 0, 0, 0].join();
    for (let y = 0; y < context.canvas.height; y++) {
        for (let x = 0; x < context.canvas.width; x++) {
            let pixel = getPixel(imageData, x, y).join();
            if (pixel != emptyPixel) {
                return y;
            }
        }
    }
}

function findBottomEdge(context) {
    let imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
    let emptyPixel = [0, 0, 0, 0].join();
    for (let y = context.canvas.height - 1; y >= 0; y--) {
        for (let x = 0; x < context.canvas.width; x++) {
            let pixel = getPixel(imageData, x, y).join();
            if (pixel != emptyPixel) {
                return y;
            }
        }
    }
}


/* Layer 1: Interacting with ImageData */

/**
 * Returns the pixel array at the specified position.
 */
function getPixel(imageData, x, y) {
    return getPixelByIndex(imageData, pos2index(imageData, x, y));
}

/**
 * Returns the RGBA values at the specified index.
 */
function getPixelByIndex(imageData, index) {
    return [
        imageData.data[index + 0],
        imageData.data[index + 1],
        imageData.data[index + 2],
        imageData.data[index + 3],
    ];
}

/**
 * Returns the index of a position.
 */
function pos2index(imageData, x, y) {
    return 4 * (y * imageData.width + x);
}
body {
    background-color: hsl(0, 0%, 95%);
}
canvas {
    background: white;
    image-rendering: pixelated;
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEXMzMz////TjRV2AAAAEUlEQVQI12P4z8CAFWEX/Q8Afr8P8erzE9cAAAAASUVORK5CYII=);
    zoom: 0.8; /* this counters the scale up (125%) of my screen; can be removed */
}
input {
    padding: 0.2em;
    margin-top: 0.5em;
}
<canvas id="canvas"></canvas>

<br>

<input type="text" id="input" placeholder="type a letter" value="A" onkeyup="drawLetter(this.value)" />

<pre id="output"></pre>

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top