1

I have a function that, given an image with a transparent background and an unknown object in it, finds the top, left, right and bottom boundaries of the object. The purpose is so that I can simply draw a box around the boundaries of the object. I'm not trying to detect the actual edges of the object - just the top most, bottom most, etc.

My function works well, but is slow because it scans every single pixel in the image.

My question is: Is there a faster, more efficient way to detected the upper-most, left-most, right-most, and bottom-most non-transparent pixel in an image, using stock PHP/GD functionality?

There's a catch that affects the options: the object in the image may have transparent parts. For example, if it's an image of a non-filled shape.

public static function getObjectBoundaries($image)
{
    // this code looks for the first non white/transparent pixel
    // from the top, left, right and bottom

    $imageInfo = array();
    $imageInfo['width'] = imagesx($image);
    $imageInfo['height'] = imagesy($image);

    $imageInfo['topBoundary'] = $imageInfo['height'];
    $imageInfo['bottomBoundary'] = 0;
    $imageInfo['leftBoundary'] = $imageInfo['width'];
    $imageInfo['rightBoundary'] = 0;

    for ($x = 0; $x <= $imageInfo['width'] - 1; $x++) {
        for ($y = 0; $y <= $imageInfo['height'] - 1; $y++) {
            $pixelColor = imagecolorat($image, $x, $y);
            if ($pixelColor != 2130706432) {        // if not white/transparent
                $imageInfo['topBoundary'] = min($y, $imageInfo['topBoundary']);
                $imageInfo['bottomBoundary'] = max($y, $imageInfo['bottomBoundary']);
                $imageInfo['leftBoundary'] = min($x, $imageInfo['leftBoundary']);
                $imageInfo['rightBoundary'] = max($x, $imageInfo['rightBoundary']);
            }
        }
    }

    return $imageInfo;
}
mmatos
  • 121
  • 1
  • 6
  • Some very interesting answers that deserve some proper testing. I will do some benchmarking in the next day or so and accept the best performing one. – mmatos Jul 21 '14 at 17:08

3 Answers3

1

Function calls in PHP are expensive. Calling imagecolorat() per pixel will absolutely ruin performance. Efficient coding in PHP means finding a built-in function that can somehow do the job. The following code makes use of the palette GD functions. At a glance it might not be intuitive but the logic is actually pretty simple: the code keeps copying the image a line of pixels at a time until it notices that it requires more than one colors to represent them.

function getObjectBoundaries2($image) {
    $width = imagesx($image);
    $height = imagesy($image);

    // create a one-pixel high image that uses a PALETTE
    $line = imagecreate($width, 1);
    for($y = 0; $y < $height; $y++) {
        // copy a row of pixels into $line
        imagecopy($line, $image, 0, 0, 0, $y, $width, 1);

        // count the number of colors in $line
        // if it's one, then assume it's the transparent color
        $count = imagecolorstotal($line);
        if($count > 1) {
            // okay, $line has employed more than one color so something's there
            // look at the first color in the palette to ensure that our initial 
            // assumption was correct 
            $firstColor = imagecolorsforindex($line, 0);
            if($firstColor['alpha'] == 127) {
                $top = $y;
            } else {
                // it was not--the first color encountered was opaque
                $top = 0;
            }
            break;
        }
    }

    if(!isset($top)) {
        // image is completely empty
        return array('width' => $width, 'height' => $height);
    }

    // do the same thing from the bottom
    $line = imagecreate($width, 1);
    for($y = $height - 1; $y > $top; $y--) {
        imagecopy($line, $image, 0, 0, 0, $y, $width, 1);
        $count = imagecolorstotal($line);
        if($count > 1) {
            $firstColor = imagecolorsforindex($line, 0);
            if($firstColor['alpha'] == 127) {
                $bottom = $y;
            } else {
                $bottom = $height - 1;
            }
            break;
        }
    }
    $nonTransparentHeight = $bottom - $top + 1;

    // scan from the left, ignoring top and bottom parts known to be transparent
    $line = imagecreate(1, $nonTransparentHeight);
    for($x = 0; $x < $width; $x++) {
        imagecopy($line, $image, 0, 0, $x, $top, 1, $nonTransparentHeight);
        $count = imagecolorstotal($line);
        if($count > 1) {
            $firstColor = imagecolorsforindex($line, 0);
            if($firstColor['alpha'] == 127) {
                $left = $x;
            } else {
                $left = 0;
            }
            break;
        }
    }

    // scan from the right
    $line = imagecreate(1, $nonTransparentHeight);
    for($x = $width - 1; $x > $left; $x--) {
        imagecopy($line, $image, 0, 0, $x, $top, 1, $nonTransparentHeight);
        $count = imagecolorstotal($line);
        if($count > 1) {
            $firstColor = imagecolorsforindex($line, 0);
            if($firstColor['alpha'] == 127) {
                $right = $x;
            } else {
                $right = $width - 1;
            }
            break;
        }
    }

    return array('width' => $width, 'height' => $height, 'topBoundary' => $top, 'bottomBoundary' => $bottom, 'leftBoundary' => $left, 'rightBoundary' => $right);
}
cleong
  • 7,242
  • 4
  • 31
  • 40
0

I think you could test the 4 sides one after an other, stopping as soon as a pixel is found. For the top boundary (untested code) :

// false so we can test it's value
$bound_top = false;
// The 2 loops have 2 end conditions, if end of row/line, or pixel found
// Loop from top to bottom
for ($y = 0; $y < $img_height && $bound_top === false; $y++) {
    // Loop from left to right (right to left would work to)
    for ($x = 0; $x < $img_width && $bound_top === false; $x++) {
        if (imageColorAt($img, $x, $y) != 2130706432) {
            $bound_top = $y;
        }
    }
}

After the loops, if $bound_top is still false, don't bother checking the other sides, you checked all pixels, the image is empty. If not, just do the same for the other sides.

Planplan
  • 794
  • 6
  • 11
0

Not every pixel needs to be examined. The following code checks columns from left to right to get leftBoundary, right to left to get rightBoundary, rows from top to bottom (while excluding pixels we've already checked) to get topBoundary, and similarly for bottomBoundary.

function get_boundary($image)
{
    $imageInfo = array();
    $imageInfo['width'] = imagesx($image);
    $imageInfo['height'] = imagesy($image);

    for ($x = 0; $x < $imageInfo['width']; $x++) {
        if (!is_box_empty($image, $x, 0, 1, $imageInfo['height'])) {
            $imageInfo['leftBoundary'] = $x;
            break;
        }
    }

    for ($x = $imageInfo['width']-1; $x >= 0; $x--) {
        if (!is_box_empty($image, $x, 0, 1, $imageInfo['height'])) {
            $imageInfo['rightBoundary'] = $x;
            break;
        }
    }

    for ($y = 0; $y < $imageInfo['height']; $y++) {
        if (!is_box_empty($image, $imageInfo['leftBoundary'], $y, $imageInfo['rightBoundary']-$imageInfo['leftBoundary']+1, 1)) {
            $imageInfo['topBoundary'] = $y;
            break;
        }
    }

    for ($y = $imageInfo['height']-1; $y >= 0; $y--) {
        if (!is_box_empty($image, $imageInfo['leftBoundary'], $y, $imageInfo['rightBoundary']-$imageInfo['leftBoundary']+1, 1)) {
            $imageInfo['bottomBoundary'] = $y;
            break;
        }
    }

    return $imageInfo;
}

function is_box_empty($image, $x, $y, $w, $h)
{
    for ($i = $x; $i < $x+$w; $i++) {
        for ($j = $y; $j < $y+$h; $j++) {
            $pixelColor = imagecolorat($image, $i, $j);
            if ($pixelColor != 2130706432) {        // if not white/transparent
                return false;
            }
        }
    }

    return true;
}
Fabricator
  • 12,722
  • 2
  • 27
  • 40