Size and position of one image in another via PHP - php

I have two images(small and big). Big one contains a small one. Like if the small one is a photo and a big one is a page from the photo album.
How do I get coordinates of that small image in the big one using PHP? And also I need to know the size of that image in big one...so just a(x,y) coordinate of any angle and sizes of sides of that presentation of the small image...
(x,y, width, height)
I've already asked the question like that and got a brilliant answer (here) but I've forgot to mention over there that the size of a small image could be different from the the size of that image in the big image...
And also if it is possible to deal with a presentation of that small image in the big image can have something covering one of its angles... Like in this example:
Small image:
Big image:
Small image always has just a rectangular shape.

Alright, this answer does not perfectly answer the question, but it should give you a good start! I know I repeat myself in the code, but my goal was simply to get something working so you can build on it, this isn't production code!
Preconditions
Starting with the large picture:
We need to find as best as possible the position of this other picture:
I decided to break the process into many substeps, which you could improve or remove depending on what you want the code to do.
For testing purposes, I did test my algorithm on different input images so you'll see a variable defining what file to load...
We start with:
function microtime_float()
{
list($usec, $sec) = explode(" ", microtime());
return ((float)$usec + (float)$sec);
}
$time_start = microtime_float();
$largeFilename = "large.jpg";
$small = imagecreatefromjpeg("small.jpg");
$large = imagecreatefromjpeg($largeFilename);
and
imagedestroy($small);
imagedestroy($large);
$time_end = microtime_float();
echo "in " . ($time_end - $time_start) . " seconds\n";
To have a good idea on our performances. Luckily, most of the algorithm was pretty fast so I didn't have to optimize more.
Background Detection
I started by detecting the background color. I assumed that the background color would be the color most present in the picture. To do this, I only counted how many references of each color I could find in the large picture, sort it with decending values and took the first one as the background color (should allow the code to be adaptable if you changed the source pictures)
function FindBackgroundColor($image)
{
// assume that the color that's present the most is the background color
$colorRefcount = array();
$width = imagesx($image);
$height = imagesy($image);
for($x = 0; $x < $width; ++$x)
{
for($y = 0; $y < $height; ++$y)
{
$color = imagecolorat($image, $x, $y);
if(isset($colorRefcount[$color]))
$colorRefcount[$color] = $colorRefcount[$color] + 1;
else
$colorRefcount[$color] = 1;
}
}
arsort($colorRefcount);
reset($colorRefcount);
return key($colorRefcount);
}
$background = FindBackgroundColor($large); // Should be white
Partitionning
My first step was to try to find all the regions where non background pixels were. With a little padding, I was able to group regions into bigger regions (so that a paragraph would be a single region instead of multiple individual letters). I started with a padding of 5 and got good enough results so I stuck with it.
This is broken into multiple function calls, so here we go:
function FindRegions($image, $backgroundColor, $padding)
{
// Find all regions within image where colors are != backgroundColor, including a padding so that adjacent regions are merged together
$width = imagesx($image);
$height = imagesy($image);
$regions = array();
for($x = 0; $x < $width; ++$x)
{
for($y = 0; $y < $height; ++$y)
{
$color = imagecolorat($image, $x, $y);
if($color == $backgroundColor)
{
continue;
}
if(IsInsideRegions($regions, $x, $y))
{
continue;
}
$region = ExpandRegionFrom($image, $x, $y, $backgroundColor, $padding);
array_push($regions, $region);
}
}
return $regions;
}
$regions = FindRegions($large, $background, 5);
Here, we iterate on every pixel of the picture, if its background color, we discard it, otherwise, we check if its position is already present in a region we found, if that's the case, we skip it too. Now, if we didn't skip the pixel, it means that it's a colored pixel that should be part of a region, so we start ExpandRegionFrom this pixel.
The code to check if we're inside a region is pretty simple:
function IsInsideRegions($regions, $x, $y)
{
foreach($regions as $region)
{
if(($region["left"] <= $x && $region["right"] >= $x) &&
($region["bottom"] <= $y && $region["top"] >= $y))
{
return true;
}
}
return false;
}
Now, the expanding code will try to grow the region in each direction and will do so as long as it found new pixels to add to the region:
function ExpandRegionFrom($image, $x, $y, $backgroundColor, $padding)
{
$width = imagesx($image);
$height = imagesy($image);
$left = $x;
$bottom = $y;
$right = $x + 1;
$top = $y + 1;
$expanded = false;
do
{
$expanded = false;
$newLeft = ShouldExpandLeft($image, $backgroundColor, $left, $bottom, $top, $padding);
if($newLeft != $left)
{
$left = $newLeft;
$expanded = true;
}
$newRight = ShouldExpandRight($image, $backgroundColor, $right, $bottom, $top, $width, $padding);
if($newRight != $right)
{
$right = $newRight;
$expanded = true;
}
$newTop = ShouldExpandTop($image, $backgroundColor, $top, $left, $right, $height, $padding);
if($newTop != $top)
{
$top = $newTop;
$expanded = true;
}
$newBottom = ShouldExpandBottom($image, $backgroundColor, $bottom, $left, $right, $padding);
if($newBottom != $bottom)
{
$bottom = $newBottom;
$expanded = true;
}
}
while($expanded == true);
$region = array();
$region["left"] = $left;
$region["bottom"] = $bottom;
$region["right"] = $right;
$region["top"] = $top;
return $region;
}
The ShouldExpand methods could have been written in a cleaner fashion, but I went for something fast to prototype with:
function ShouldExpandLeft($image, $background, $left, $bottom, $top, $padding)
{
// Find the farthest pixel that is not $background starting at $left - $padding closing in to $left
for($x = max(0, $left - $padding); $x < $left; ++$x)
{
for($y = $bottom; $y <= $top; ++$y)
{
$pixelColor = imagecolorat($image, $x, $y);
if($pixelColor != $background)
{
return $x;
}
}
}
return $left;
}
function ShouldExpandRight($image, $background, $right, $bottom, $top, $width, $padding)
{
// Find the farthest pixel that is not $background starting at $right + $padding closing in to $right
$from = min($width - 1, $right + $padding);
$to = $right;
for($x = $from; $x > $to; --$x)
{
for($y = $bottom; $y <= $top; ++$y)
{
$pixelColor = imagecolorat($image, $x, $y);
if($pixelColor != $background)
{
return $x;
}
}
}
return $right;
}
function ShouldExpandTop($image, $background, $top, $left, $right, $height, $padding)
{
// Find the farthest pixel that is not $background starting at $top + $padding closing in to $top
for($x = $left; $x <= $right; ++$x)
{
for($y = min($height - 1, $top + $padding); $y > $top; --$y)
{
$pixelColor = imagecolorat($image, $x, $y);
if($pixelColor != $background)
{
return $y;
}
}
}
return $top;
}
function ShouldExpandBottom($image, $background, $bottom, $left, $right, $padding)
{
// Find the farthest pixel that is not $background starting at $bottom - $padding closing in to $bottom
for($x = $left; $x <= $right; ++$x)
{
for($y = max(0, $bottom - $padding); $y < $bottom; ++$y)
{
$pixelColor = imagecolorat($image, $x, $y);
if($pixelColor != $background)
{
return $y;
}
}
}
return $bottom;
}
Now, to see if the algorithm was succesful, I added some debug code.
Debug Rendering
I created a second image to store debug info and store it on disk so I could later see my progress.
Using the following code:
$large2 = imagecreatefromjpeg($largeFilename);
$red = imagecolorallocate($large2, 255, 0, 0);
$green = imagecolorallocate($large2, 0, 255, 0);
$blue = imagecolorallocate($large2, 0, 0, 255);
function DrawRegions($image, $regions, $color)
{
foreach($regions as $region)
{
imagerectangle($image, $region["left"], $region["bottom"], $region["right"], $region["top"], $color);
}
}
DrawRegions($large2, $regions, $red);
imagejpeg($large2, "regions.jpg");
I could validate that my partitioning code was doing a decent job:
Aspect Ratio
I decided to filter out some regions based on aspect ratio (the ratio between the width and the height). Other filtering could be applied such as average pixel color or something, but the aspect ratio check was very fast so I used it.
I simply defined a "window" where regions would be kept, if their aspect ration was between a minimum and maximum value;
$smallAspectRatio = imagesx($small) / imagesy($small);
function PruneOutWrongAspectRatio($regions, $minAspectRatio, $maxAspectRatio)
{
$result = array();
foreach($regions as $region)
{
$aspectRatio = ($region["right"] - $region["left"]) / ($region["top"] - $region["bottom"]);
if($aspectRatio >= $minAspectRatio && $aspectRatio <= $maxAspectRatio)
{
array_push($result, $region);
}
}
return $result;
}
$filterOnAspectRatio = true;
if($filterOnAspectRatio == true)
{
$regions = PruneOutWrongAspectRatio($regions, $smallAspectRatio - 0.1 * $smallAspectRatio, $smallAspectRatio + 0.1 * $smallAspectRatio);
DrawRegions($large2, $regions, $blue);
}
imagejpeg($large2, "aspectratio.jpg");
By adding the DrawRegions call, I now paint in blue the regions that are still in the list as potential positions:
As you can see, only 4 position remains!
Finding the Corners
We're almost done! Now, what I'm doing is looking at the colors in the four corners from the small picture, and try to find the best matching pixel in the corners of the remaining regions. This code has the most potential to fail so if you have to invest time in improving the solution, this code would be a good candidate.
function FindCorners($large, $small, $regions)
{
$result = array();
$bottomLeftColor = imagecolorat($small, 0, 0);
$blColors = GetColorComponents($bottomLeftColor);
$bottomRightColor = imagecolorat($small, imagesx($small) - 1, 0);
$brColors = GetColorComponents($bottomRightColor);
$topLeftColor = imagecolorat($small, 0, imagesy($small) - 1);
$tlColors = GetColorComponents($topLeftColor);
$topRightColor = imagecolorat($small, imagesx($small) - 1, imagesy($small) - 1);
$trColors = GetColorComponents($topRightColor);
foreach($regions as $region)
{
$bottomLeft = null;
$bottomRight = null;
$topLeft = null;
$topRight = null;
$regionWidth = $region["right"] - $region["left"];
$regionHeight = $region["top"] - $region["bottom"];
$maxRadius = min($regionWidth, $regionHeight);
$topLeft = RadialFindColor($large, $tlColors, $region["left"], $region["top"], 1, -1, $maxRadius);
$topRight = RadialFindColor($large, $trColors, $region["right"], $region["top"], -1, -1, $maxRadius);
$bottomLeft = RadialFindColor($large, $blColors, $region["left"], $region["bottom"], 1, 1, $maxRadius);
$bottomRight = RadialFindColor($large, $brColors, $region["right"], $region["bottom"], -1, 1, $maxRadius);
if($bottomLeft["found"] && $topRight["found"] && $topLeft["found"] && $bottomRight["found"])
{
$left = min($bottomLeft["x"], $topLeft["x"]);
$right = max($bottomRight["x"], $topRight["x"]);
$bottom = min($bottomLeft["y"], $bottomRight["y"]);
$top = max($topLeft["y"], $topRight["y"]);
array_push($result, array("left" => $left, "right" => $right, "bottom" => $bottom, "top" => $top));
}
}
return $result;
}
$closeOnCorners = true;
if($closeOnCorners == true)
{
$regions = FindCorners($large, $small, $regions);
DrawRegions($large2, $regions, $green);
}
I tried to find the matching color by increasing "radially" (its basically squares) from the corners until I find a matching pixel (within a tolerance):
function GetColorComponents($color)
{
return array("red" => $color & 0xFF, "green" => ($color >> 8) & 0xFF, "blue" => ($color >> 16) & 0xFF);
}
function GetDistance($color, $r, $g, $b)
{
$colors = GetColorComponents($color);
return (abs($r - $colors["red"]) + abs($g - $colors["green"]) + abs($b - $colors["blue"]));
}
function RadialFindColor($large, $color, $startx, $starty, $xIncrement, $yIncrement, $maxRadius)
{
$result = array("x" => -1, "y" => -1, "found" => false);
$treshold = 40;
for($r = 1; $r <= $maxRadius; ++$r)
{
$closest = array("x" => -1, "y" => -1, "distance" => 1000);
for($i = 0; $i <= $r; ++$i)
{
$x = $startx + $i * $xIncrement;
$y = $starty + $r * $yIncrement;
$pixelColor = imagecolorat($large, $x, $y);
$distance = GetDistance($pixelColor, $color["red"], $color["green"], $color["blue"]);
if($distance < $treshold && $distance < $closest["distance"])
{
$closest["x"] = $x;
$closest["y"] = $y;
$closest["distance"] = $distance;
break;
}
}
for($i = 0; $i < $r; ++$i)
{
$x = $startx + $r * $xIncrement;
$y = $starty + $i * $yIncrement;
$pixelColor = imagecolorat($large, $x, $y);
$distance = GetDistance($pixelColor, $color["red"], $color["green"], $color["blue"]);
if($distance < $treshold && $distance < $closest["distance"])
{
$closest["x"] = $x;
$closest["y"] = $y;
$closest["distance"] = $distance;
break;
}
}
if($closest["distance"] != 1000)
{
$result["x"] = $closest["x"];
$result["y"] = $closest["y"];
$result["found"] = true;
return $result;
}
}
return $result;
}
As you can see, I'm no PHP expert, I didn't know there was a built in function to get the rgb channels, oops!
Final Call
So now that the algorithm ran, let's see what it found using the following code:
foreach($regions as $region)
{
echo "Potentially between " . $region["left"] . "," . $region["bottom"] . " and " . $region["right"] . "," . $region["top"] . "\n";
}
imagejpeg($large2, "final.jpg");
imagedestroy($large2);
The output (which is pretty close to the real solution):
Potentially between 108,380 and 867,827
in 7.9796848297119 seconds
Giving this picture (the rectangle between 108,380 and 867,827 is drawn in green)
Hope this helps!

My solution work if there is no color (except white and black around the image, but you can modify the script to get it work differently)
$width = imagesx($this->img_src);
$height = imagesy($this->img_src);
// navigate through pixels of image
for ($y = 0; $y < $height; $y++) {
for ($x=0; $x < $width; $x++) {
list($r, $g, $b) = imagergbat($this->img_src, $x, $y);
$black = 0.1;
$white = 0.9;
// calculate if the color is next to white or black, if not register it as a good pixel
$gs = (($r / 3) + ($g / 3) + ($b / 3);
$first_pixel = array();
if ($gs > $white && $gs < $black) {
// get coordinate of first pixel (left top)
if (empty($first_pixel))
$first_pixel = array($x, $y);
// And save last_pixel each time till the last one
$last_pixel = array($x, $y);
}
}
}
And you get the coordinates of your image. You have just to crop it after this.

Related

php gd image png watermark on jpg file produces weird result

Trying to add a transparent PNG watermark to jpg images get's me different results either if i'm on localhost or production server.
Here's the results...
On my localhost (PHP Version 5.6.24)
On production server (PHP Version 5.5.9-1ubuntu4.21)
I don't even know what to call this problem of the blue color turning pink and the logo doesn't get correctly rendered...
The code is this:
function getBrightness($gdHandle) {
$width = imagesx($gdHandle);
$height = imagesy($gdHandle);
$totalBrightness = 0;
for ($x = 0; $x < $width; $x++) {
for ($y = 0; $y < $height; $y++) {
$rgb = imagecolorat($gdHandle, $x, $y);
$red = ($rgb >> 16) & 0xFF;
$green = ($rgb >> 8) & 0xFF;
$blue = $rgb & 0xFF;
$totalBrightness += (max($red, $green, $blue) + min($red, $green, $blue)) / 2;
}
}
// imagedestroy($gdHandle);
return ($totalBrightness / ($width * $height)) / 2.55;
}
function render_text_on_gd_image(&$source_gd_image, $text, $font, $size, $color, $opacity, $rotation, $bpx, $bpy, $wnewx) {
$source_width = imagesx($source_gd_image);
$source_height = imagesy($source_gd_image);
$bb = imagettfbbox($size, $rotation, $font, $text);
$x0 = min($bb[0], $bb[2], $bb[4], $bb[6]);
$x1 = max($bb[0], $bb[2], $bb[4], $bb[6]);
$y0 = min($bb[1], $bb[3], $bb[5], $bb[7]);
$y1 = max($bb[1], $bb[3], $bb[5], $bb[7]);
$bb_width = abs($x1 - $x0);
$bb_height = abs($y1 - $y0);
$alpha_color = imagecolorallocatealpha(
$source_gd_image,
hexdec(substr($color, 0, 2)),
hexdec(substr($color, 2, 2)),
hexdec(substr($color, 4, 2)),
127 * (100 - $opacity) / 100
);
return imagettftext($source_gd_image, $size, $rotation, $bpx + $wnewx/2 - $bb_width/2 , $bpy, $alpha_color, $font, $text);
}
$val = array();
//$val["displayName"] = "Administrador";
$val["displayName"] = "Afonso Ferreira Gomes";
$val["relFile"] = "imagem.jpg";
list($val["width_original"], $val["height_original"]) = getimagesize($val["relFile"]);
if ($val["width_original"] > $val["height_original"] && $val["width_original"] >= 1200) {
$val["height_resized"] = $val["height_original"] * (1200/$val["width_original"]);
$val["width_resized"] = 1200;
} elseif ($val["height_original"] > $val["width_original"] && $val["height_original"] >= 1200) {
$val["height_resized"] = 1200;
$val["width_resized"] = (1200/$val["height_original"]) * $val["width_original"];
} else {
$val["height_resized"] = $val["height_original"];
$val["width_resized"] = $val["width_original"];
}
$i = imagecreatefromjpeg($val["relFile"]);
$i = imagescale($i, $val["width_resized"], $val["height_resized"]);
$val["width_new"] = imagesx($i);
$val["height_new"] = imagesy($i);
if ($val["width_new"] < $val["height_new"]) {
$val["portait"] = true;
$val["smallest"] = $val["width_new"];
} else {
$val["portait"] = false;
$val["smallest"] = $val["height_new"];
}
/* GERAR WATERMARK LOGOTIPO */
$val["racio"] = 0.20;
$val["marLeft"] = 40;
$val["marBottom"] = 40;
$val["bright"] = getBrightness($i);
if ($val["bright"] < 30) {
$val["opacidade"] = 20;
} else {
$val["opacidade"] = 40;
}
$w = imagecreatefrompng("jbw_260_40.png");
if ($val["portait"]) {
$val["water_width"] = $val["width_new"] * $val["racio"];
$val["water_height"] = $val["width_new"] * $val["racio"];
} else {
$val["water_width"] = $val["height_new"] * $val["racio"];
$val["water_height"] = $val["height_new"] * $val["racio"];
}
$wnew = imagescale($w, $val["water_width"], $val["water_height"]);
$val["water_width"] = imagesx($wnew);
$val["water_height"] = imagesy($wnew);
/* GERAR WATERMARK TEXTO AUTOR */
if ($val["displayName"] == "Administrador") {
$val["autor"] = "";
$val["marBottom"] = 20;
$val["marLeft"] = 20;
} else {
$val["autor"] = $val["displayName"];
}
// PRIMEIRO E ULTIMO NOME SÓ!!
$autorNomes = explode(" ", $val["autor"]);
if (count($autorNomes) >= 2) {
$val["autor"] = $autorNomes[0] . " " . $autorNomes[count($autorNomes) - 1];
} else {
$val["autor"] = $autorNomes[0];
}
$font = "../font/Montserrat.ttf";
$color = "FFFFFF";
$flagSize = false;
$val["fontSize"] = 12;
for ($size = 7; $size <= 50; $size += 0.5) {
$bb = imagettfbbox($size, 0, $font, $val["autor"]);
$x0 = min($bb[0], $bb[2], $bb[4], $bb[6]);
$x1 = max($bb[0], $bb[2], $bb[4], $bb[6]);
$y0 = min($bb[1], $bb[3], $bb[5], $bb[7]);
$y1 = max($bb[1], $bb[3], $bb[5], $bb[7]);
$val["bbox_width"] = abs($x1 - $x0);
$val["bbox_height"] = abs($y1 - $y0);
if (($val["bbox_width"] >= $val["water_width"] && !$flagSize) || $size == 50) {
$val["fontSize"] = $size;
$flagSize = true;
break;
}
}
$val["posY_water"] = $val["height_new"] - $val["water_height"] - $val["marBottom"];
$val["posY_text"] = $val["height_new"] - $val["marBottom"] + $val["bbox_height"] - 5;
imagecopy($i, $wnew, $val["marLeft"], $val["posY_water"] , 0, 0, $val["water_width"], $val["water_height"]);
render_text_on_gd_image($i, $val["autor"], $font, $val["fontSize"], $color, $val["opacidade"] ,0 , $val["marLeft"] , $val["posY_text"], $val["water_width"]);
// echo "<pre>"; print_r($val); echo "</pre>"; die;
/* OUTPUT IMAGEM E LIMPA BUFFER */
header('Content-type: image/png');
imagepng($i);
imagedestroy($i);
imagedestroy($w);
And this are the gd section of phpinfo for localhost and production server
What am I missing? this is driving me insane!
Those issues almost always object to the alpha channel of either the source or destination not beeing preserved or the output not really beeing true color.
Try using the imagesavealpha() function and search for "alpha" in the comments of the PHP documentation because there are many people having solutions to similiar issues.
This comment might be helpful and is kinda what Esko said:
http://php.net/manual/en/function.imagecreatefrompng.php#90364
Basically it copies an image to a buffer using imagecreatetruecolor().
I'm having the same issue. The imageScale function in Ubuntu seems to turn greens and blues to pink. Solution could be to use imagecopyresampled and imagecreatetruecolor. However this solution is way slower.

Find the 4 coordinates of a rotated transparent rectangle in png image

I didn't find anything about the subject, but I am dreaming vividly or is it possible in PHP to scan a PNG image and find the transparent positions in a picture?
for example, if there is a image of a TV with a transparent hole where the screen is. Can I find the most top-left, most top-right, most bottom-left, most bottom-right coordinates of transparent pixels by scanning the alpha-channel?
Not sure if there's a library doing this, I checked real quick but did not find..
Maybe not the most elegant solution and I'm sure there's a better way of doing it, but this works for a well-formed png image
// Returns the coordinates of a transparent rectangle in a PNG file (top left, top right, lower left, lower right
public function getTransparentRectangleCoordinates($fileUrl)
{
define ('TRANSPARENCY_THRESHOLD', 100); // 127=fully transparent, 0=black
$img = #imagecreatefrompng($fileUrl);
if (!$img) return ('Invalid PNG Image');
$coordLowestX = array(imagesx($img), '');
$coordHighestX = array(0, '');
$coordLowestY = array('', imagesy($img));
$coordHighestY = array('', 0);
$minX = imagesx($img);
$maxX = 0;
$minY = imagesy($img);
$maxY = 0;
// Scanning image pixels to find transparent points
for ($x=0; $x < imagesx($img); ++$x)
{
for ($y=0; $y < imagesy($img); ++$y)
{
$alpha = (imagecolorat($img, $x, $y) >> 24) & 0xFF;
if ($alpha >= TRANSPARENCY_THRESHOLD)
{
if ($x < $coordLowestX[0]) $coordLowestX = array($x, $y);
if ($x > $coordHighestX[0]) $coordHighestX = array($x, $y);
if ($y < $coordLowestY[1]) $coordLowestY = array($x, $y);
if ($y >= $coordHighestY[1]) $coordHighestY = array($x, $y);
if ($x < $minX) $minX = $x;
if ($x > $maxX) $maxX = $x;
if ($y < $minY) $minY = $y;
if ($y > $maxY) $maxY = $y;
}
}
}
// This means it's a non-rotated rectangle
if ( $coordLowestX == array($minX, $minY) )
{
$isRotated = false;
return array( array($minX, $minY), array($maxX, $minY), array($minX, $maxY), array($maxX, $maxY) );
}
// This means it's a rotated rectangle
else
{
$isRotated = true;
// Rotated counter-clockwise
if ($coordLowestX[1] < $coordHighestX[1])
return array($coordLowestX, $coordLowestY, $coordHighestY, $coordHighestX);
else // Rotated clockwise
return array($coordLowestY, $coordHighestY, $coordLowestX, $coordHighestX);
}
}

How can I optimize this image "edge detection" algorithm?

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;
}
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);
}
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.
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;
}

Multiply filter with PHP's GD library

I've tried experimenting with the GD library to simulate Photoshop's muliply effect, but I haven't found a working solution yet.
According to Wikipedia, the multiply blend mode:
[...] multiplies the numbers for each pixel of the top layer with the corresponding pixel for the bottom layer. The result is a darker picture.
Does anyone know of a way to achieve this using PHP? Any help would be much appreciated.
You need to take every pixel of your image, then multiply each RGB value with your background color / 255 (it's the Photoshop formula). For example, a JPG file with a red background color multiply filter, saved as a PNG file for better results:
<?php
$filter_r=216;
$filter_g=0;
$filter_b=26;
$suffixe="_red";
$path=YOURPATHFILE;
if(is_file($path)){
$image=#imagecreatefromjpeg($path);
$new_path=substr($path,0,strlen($path)-4).$suffixe.".png";
$imagex = imagesx($image);
$imagey = imagesy($image);
for ($x = 0; $x <$imagex; ++$x) {
for ($y = 0; $y <$imagey; ++$y) {
$rgb = imagecolorat($image, $x, $y);
$TabColors=imagecolorsforindex ( $image , $rgb );
$color_r=floor($TabColors['red']*$filter_r/255);
$color_g=floor($TabColors['green']*$filter_g/255);
$color_b=floor($TabColors['blue']*$filter_b/255);
$newcol = imagecolorallocate($image, $color_r,$color_g,$color_b);
imagesetpixel($image, $x, $y, $newcol);
}
}
imagepng($image,$new_path);
}
?>
I've been looking for Multiply blend between two images as well and couldn't find any native-php solution for it. It appears that only way (for now) is to "manually" set pixels, pixel-by-pixel. Here's my code that does Multiply blend between two images, assuming that images are of the same size. You can adjust it to handle different sizes if you like.
function multiplyImage($dst,$src)
{
$ow = imagesx($dst);
$oh = imagesy($dst);
$inv255 = 1.0/255.0;
$c = imagecreatetruecolor($ow,$oh);
for ($x = 0; $x <$ow; ++$x)
{
for ($y = 0; $y <$oh; ++$y)
{
$rgb_src = imagecolorsforindex($src,imagecolorat($src, $x, $y));
$rgb_dst = imagecolorsforindex($dst,imagecolorat($dst, $x, $y));
$r = $rgb_src['red'] * $rgb_dst['red']*$inv255;
$g = $rgb_src['green'] * $rgb_dst['green']*$inv255;
$b = $rgb_src['blue'] * $rgb_dst['blue']*$inv255;
$rgb = imagecolorallocate($c,$r,$g,$b);
imagesetpixel($c, $x, $y, $rgb);
}
}
return $c;
}
Function returns image object so you should ensure to do imagedestroy after you're done using it.
There should be a workaround using overlay native-php blend, which suggests that 50% gray pixels of destination image will be affected by source pixels. In theory, if you do need to blend two black-and-white images (no gray tones), if you set contrast of destination image so white will become 50%-gray, and then overlay-blend source image over it, should give you something similar to multiply. But for color images, or grayscale images, this wouldn't work - above method appears to be the only option.
I was led into this thread when I needed to blend two images in GD. It seems there is no code specifically for that so I will just leave this here for future visitors to this page.
This is a fork from the answer of colivier that supports multiply-blending of two images.
The two images need not be of the same size BUT the overlaying image will be resized and cropped to the size of the bottom layer. I made a fit helper function to do just that but don't bother with that.
imagecolorat returns the base color, even with PNGs with transparency. That is, a 50% black (visible as (128, 128, 128)) will be returned as (0, 0, 0, 64) 64 being the alpha value. This code takes into consideration translucency and converts the translucent colors to the visible color values.
// bottom layer
$img1 = imagecreatefromjpeg(realpath(__DIR__.'/profilePic.jpg'));
// top layer
$img2 = imagecreatefrompng(realpath(__DIR__.'/border2.png'));
imagealphablending($img2, false);
imagesavealpha($img2, true);
$imagex = imagesx($img1);
$imagey = imagesy($img1);
$imagex2 = imagesx($img2);
$imagey2 = imagesy($img2);
// Prereq: Resize img2 to match img1, cropping beyond the aspect ratio
$w1 = max(min($imagex2, $imagex), $imagex);
$h1 = max(min($imagey2, $imagey), $imagey);
$w_using_h1 = round($h1 * $imagex2 / $imagey2);
$h_using_w1 = round($w1 * $imagey2 / $imagex2);
if ($w_using_h1 > $imagex) {
fit($img2, $imagex, $imagey, 'HEIGHT', true);
}
fit($img2, $imagex, $imagey, 'WIDTH', true);
// Actual multiply filter
for ($x = 0; $x < $imagex; ++$x) {
for ($y = 0; $y < $imagey; ++$y) {
$rgb1 = imagecolorat($img1, $x, $y);
$rgb2 = imagecolorat($img2, $x, $y);
$idx1 = imagecolorsforindex($img1, $rgb1);
$idx2 = imagecolorsforindex($img2, $rgb2);
// Shift left 8, then shift right 7
// same as multiply by 256 then divide by 128
// approximate multiply by 255 then divide by 127
// This is basically multiply by 2 but, expanded to show that
// we are adding a fraction of white to the translucent image
// $adder = ($idx2['alpha'] << 8 >> 7);
$adder = ($idx2['alpha'] << 1);
$rmul = min(255, $idx2['red'] + $adder);
$gmul = min(255, $idx2['green'] + $adder);
$bmul = min(255, $idx2['blue'] + $adder);
$color_r = floor($idx1['red'] * $rmul / 255);
$color_g = floor($idx1['green'] * $gmul / 255);
$color_b = floor($idx1['blue'] * $bmul / 255);
$newcol = imagecolorallocatealpha($img1, $color_r, $color_g, $color_b, 0);
imagesetpixel($img1, $x, $y, $newcol);
}
}
imagejpeg($img1, __DIR__.'/out.jpg');
/**
* Fits an image to a $w x $h canvas
*
* #param type $w Target width
* #param type $h Target height
* #param int $fit_which Which dimension to fit
* #param bool $upscale If set to true, will scale a smaller image to fit the given dimensions
* #param bool $padded If set to true, will add padding to achieve given dimensions
*
* #return Image object
*/
function fit(&$img, $w, $h, $fit_which = 'BOTH', $upscale = false, $padded = true) {
if (!in_array($fit_which, array('WIDTH', 'HEIGHT', 'BOTH'))) {
$fit_which = 'BOTH';
}
$w0 = imagesx($img);
$h0 = imagesy($img);
if (!$upscale && $w0 <= $w && $h0 <= $h)
return $this;
if ($padded) {
$w1 = max(min($w0, $w), $w);
$h1 = max(min($h0, $h), $h);
}
else {
$w1 = min($w0, $w);
$h1 = min($h0, $h);
}
$w_using_h1 = round($h1 * $w0 / $h0);
$h_using_w1 = round($w1 * $h0 / $w0);
// Assume width, crop height
if ($fit_which == 'WIDTH') {
$w2 = $w1;
$h2 = $h_using_w1;
}
// Assume height, crop width
elseif ($fit_which == 'HEIGHT') {
$w2 = $w_using_h1;
$h2 = $h1;
}
elseif ($fit_which == 'BOTH') {
if (!$padded) {
$w2 = $w = min($w, $w_using_h1);
$h2 = $h = min($h, $h_using_w1);
}
else {
// Extend vertically
if ($h_using_w1 <= $h) {
$w2 = $w1;
$h2 = $h_using_w1;
}
// Extend horizontally
else {
$w2 = $w_using_h1;
$h2 = $h1;
}
}
}
$im2 = imagecreatetruecolor($w, $h);
imagealphablending($im2, true);
imagesavealpha($im2, true);
$transparent = imagecolorallocatealpha($im2, 255, 255, 255, 127);
imagefill($im2, 0, 0, $transparent);
imagealphablending($img, true);
imagesavealpha($img, true);
// imagefill($im, 0, 0, $transparent);
imagecopyresampled($im2, $img, ($w - $w2) / 2, ($h - $h2) / 2, 0, 0, $w2, $h2, $w0, $h0);
$img = $im2;
}
Have you tried to use php manual?
For people looking to apply a 'multiply' effect on images like the one in Photoshop (generally b&w ones), you can achieve it with the IMG_FILTER_COLORIZE filter.
<?php
function multiplyColor(&$im, $color = array(255, 0, 0)) {
//get opposite color
$opposite = array(255 - $color[0], 255 - $color[1], 255 - $color[2]);
//now we subtract the opposite color from the image
imagefilter($im, IMG_FILTER_COLORIZE, -$opposite[0], -$opposite[1], -$opposite[2]);
}
?>
If used with png image and alpha must be well and works very well
$filter_r=215;
$filter_g=5;
$filter_b=5;
$alpha=70;
$suffixe="_red";
$path="./img/foto_220_590.png";
if(is_file($path)){
$image=imagecreatefrompng($path);
$new_path=substr($path,0,strlen($path)-4).$suffixe.".png";
echo $imagex = imagesx($image);
echo $imagey = imagesy($image);
for ($x = 0; $x <$imagex; ++$x) {
for ($y = 0; $y <$imagey; ++$y) {
$rgb = imagecolorat($image, $x, $y);
$TabColors=imagecolorsforindex ( $image , $rgb );
$color_r=floor($TabColors['red']*$filter_r/255);
$color_g=floor($TabColors['green']*$filter_g/255);
$color_b=floor($TabColors['blue']*$filter_b/255);
//$newcol = imagecolorallocate($image, $color_r,$color_g,$color_b);
// this new alpha
$newcol = imagecolorallocatealpha($image, $color_r,$color_g,$color_b,$alpha);
imagesetpixel($image, $x, $y, $newcol);
}
}
imagepng($image,$new_path);
I updated #colivier script to be able to myltiply two images, and not just an image with a color:
/**
* Multiply $pathToDst and $pathToSrc to $resultPath
*
* #param string $pathToDst
* #param string $pathToSrc
* #param string $resultPath
*/
function multiply($pathToDst, $pathToSrc, $resultPath) {
switch (pathinfo($pathToDst, PATHINFO_EXTENSION)) {
case "gif" :
$resourceDst = imagecreatefromgif($pathToDst);
break;
case "png" :
$resourceDst = imagecreatefrompng($pathToDst);
break;
default :
$resourceDst = imagecreatefromjpeg($pathToDst);
break;
}
switch (pathinfo($pathToSrc, PATHINFO_EXTENSION)) {
case "gif" :
$resourceSrc = imagecreatefromgif($pathToSrc);
break;
case "png" :
$resourceSrc = imagecreatefrompng($pathToSrc);
break;
default :
$resourceSrc = imagecreatefromjpeg($pathToSrc);
break;
}
for ($x = 0; $x < 400; ++$x) {
for ($y = 0; $y < 400; ++$y) {
$TabColorsFlag = imagecolorsforindex($resourceDst, imagecolorat($resourceDst, $x, $y));
$TabColorsPerso = imagecolorsforindex($resourceSrc, imagecolorat($resourceSrc, $x, $y));
$color_r = floor($TabColorsFlag['red'] * $TabColorsPerso['red'] / 255);
$color_g = floor($TabColorsFlag['green'] * $TabColorsPerso['green'] / 255);
$color_b = floor($TabColorsFlag['blue'] * $TabColorsPerso['blue'] / 255);
imagesetpixel($resourceDst, $x, $y, imagecolorallocate($resourceSrc, $color_r, $color_g, $color_b));
}
}
imagepng($resourceDst, $resultPath, 0);
imagedestroy($resourceDst);
imagedestroy($resourceSrc);
}

imagettfbbox() returns wrong dimensions when using space characters inside text

I'm creating a graphical menu dynamically from PHP, it results in only 1 image that looks like this:
One Two Three Four
The problem however is, that I have to determine the x-offset and the width of each page title (e.g. the left offset and the width of "One" in pixels) to position the image with css.
All works fine, except for page titles that contain spaces - imagettfbbox() returns the wrong positions. I am using an arial font (TTF)
Any suggestions on how I get around this issue?
Edit: I got it working now, using the following function to determine the text's bounds:
function calculateTextBox($font_size, $font_angle, $font_file, $text) {
$box = imagettfbbox($font_size, $font_angle, $font_file, $text);
$min_x = min(array($box[0], $box[2], $box[4], $box[6]));
$max_x = max(array($box[0], $box[2], $box[4], $box[6]));
$min_y = min(array($box[1], $box[3], $box[5], $box[7]));
$max_y = max(array($box[1], $box[3], $box[5], $box[7]));
return array(
'left' => ($min_x >= -1) ? -abs($min_x + 1) : abs($min_x + 2),
'top' => abs($min_y),
'width' => $max_x - $min_x,
'height' => $max_y - $min_y,
'box' => $box
);
}
Edit 2: Unfortunately I keep getting wrong dimensions when using different font sizes and font files...
One way to do it would be to use the imagecolorat function to work out the colour of a pixel at particular point in the image, its not going to be the quickest or even necessarily the best way to do it but it should work.
Something like this (searching for black pixels) should work, and will return the bounds you're looking for which you can then translate into x/ co-ordinates if you want:
function get_bounds($image)
{
$height = imagesy($image);
$width = imagesx($image);
$to_return = new stdClass();
$to_return->top = null;
$to_return->bottom = null;
$to_return->left = null;
$to_return->right = null;
for ($x = 0; $x < $width; $x++)
{
for ($y = 0; $y < $height; $y++)
{
$color = imagecolorat($image, $x, $y);
$rgb = imagecolorsforindex($image, $color);
// If its black
if (($rgb['red'] == 0) && ($rgb['green'] == 0) && ($rgb['blue'] == 0))
{
if (($y < $to_return->top) || is_null($to_return->top))
{
$to_return->top = $y;
}
if (($y > $to_return->bottom) || is_null($to_return->bottom))
{
$to_return->bottom = $y;
}
if (($x < $to_return->left) || is_null($to_return->left))
{
$to_return->left = $x;
}
if (($x > $to_return->right) || is_null($to_return->right))
{
$to_return->right = $x;
}
}
}
}
return $to_return;
}
Actually, its real bounds can be calculated from imagettftext function's return values.

Categories