I need to convert a lot of x/y pixel coordinates from polygon segments (680x680) to the grid references (68x68) the polygon contains.
e.g grid references
1, 2, 3, 4
5, 6, 7, 8
9,10,11,12 etc
Performance is the ultimate goal. My working script does the job however with thousands of sets of polygon segments each minute I'm looking to improve speed further. Currently I'm using the GD library to draw a polygon, then with help of a bounding box, testing the brightness of each polygon pixel to get x/y coordinates, then finally converting those to a grid reference.
While the overhead of generating an image in memory isn't huge, there must be a better (or faster) way to do this.
Working example with output
$p = [];
$r = [];
$p['segments'] = [[144, 637], [225, 516], [85, 460], [30, 482]];
$r = segments_to_grid($p, $r);
print_r($r['grid']);
Array
(
[0] => 3133
[1] => 3134
[2] => 3135
[3] => 3136
[4] => 3137
[5] => 3138
[6] => 3199
[7] => 3200
...
...
[157] => 4092
[158] => 4093
[159] => 4094
[160] => 4095
[161] => 4161
[162] => 4162
[163] => 4229
)
Supporting functions
/**
* Convert a list of x/y coordinates to grid references
*
* #param array $p
* #param array $r
*
* #return array augmented $r
*/
function segments_to_grid($p, $r) {
$p['segments'] = isset($p['segments']) ? $p['segments'] : [];
// e.g, [[144,637],[225,516],[85,460],[30,482]]
// Return array
$r['grid'] = [];
// Define base dimensions
$w = 680;
$h = 680;
$poly_coords = [];
$min_x = $min_y = 680;
$max_x = $max_y = 0;
// Build an imagefilledpolygon compatible array and extract minimum and maximum for bounding box
foreach ($p['segments'] as $segment) {
$poly_coords[] = $segment[0];
$poly_coords[] = $segment[1];
$min_x = min($min_x, $segment[0]);
$min_y = min($min_y, $segment[1]);
$max_x = max($max_x, $segment[0]);
$max_y = max($max_y, $segment[1]);
}
// check we have something useful
if (!empty($poly_coords)) {
$r['code'] = 40;
// create image
$img = imagecreatetruecolor($w, $h);
// allocate colors (white background, black polygon)
$bg = imagecolorallocate($img, 255, 255, 255);
$black = imagecolorallocate($img, 0, 0, 0);
// fill the background
imagefilledrectangle($img, 0, 0, $w, $h, $bg);
// draw a polygon
if (imagefilledpolygon($img, $poly_coords, count($p['segments']), $black)) {
$r['code'] = 0;
// loop through the image and find the points that are black
for ($y = $min_y; $y < $max_y; $y = $y + 10) {
for ($x = $min_x; $x < $max_x; $x = $x + 10) {
$rgb = imagecolorat($img, $x, $y);
if (intval($rgb) < 16777215) {
$r['grid'][] = xy6802g68($x, $y);
}
}
}
} else {
$r['error'] = 'poly fail';
$r['code'] = 10;
}
imagedestroy($img);
} else {
$r['error'] = 'no coordinates';
$r['code'] = 20;
}
return ($r);
}
/**
* Converts X/Y 680x680 to 68x68 grid reference number.
*
* #param int $cX pixel x positon
* #param int $cY pixel y positon
* #return int grid reference number
*/
function xy6802g68($cX, $cY) {
$calcX = ceil($cX / 10) - 1;
$calcY = ceil($cY / 10) - 1;
$grid68 = $calcX + ($calcY * 68);
return ($grid68);
}
Solution thanks to #Oliver's comments.
Porting inpoly to PHP was 168 times faster than using GD image lib.
function segments_to_grid2($p, $r) {
// Define base dimensions
$vertx = $verty = [];
$min_x = $min_y = 680;
$max_x = $max_y = 0;
foreach ($p['segments'] as $segment) {
$vertx[] = $segment[0];
$verty[] = $segment[1];
$min_x = min($min_x, $segment[0]);
$min_y = min($min_y, $segment[1]);
$max_x = max($max_x, $segment[0]);
$max_y = max($max_y, $segment[1]);
}
if (!empty($vertx)) {
$nvert = count($vertx);
for ($y = $min_y; $y < $max_y; $y = $y + 10) {
for ($x = $min_x; $x < $max_x; $x = $x + 10) {
if (inpoly($nvert, $vertx, $verty, $x, $y)) {
$r['grid'][] = xy6802g68($x, $y);
}
}
}
}
return $r;
}
function inpoly($nvert, $vertx, $verty, $testx, $testy) {
$i = $j = $c = 0;
for ($i = 0, $j = $nvert - 1; $i < $nvert; $j = $i++) {
if ((($verty[$i] > $testy) != ($verty[$j] > $testy)) && ($testx < ($vertx[$j] - $vertx[$i]) * ($testy - $verty[$i]) / ($verty[$j] - $verty[$i]) + $vertx[$i])) {
$c = !$c;
}
}
return $c;
}
Running this 100 times
GD library version:
[segments_to_grid] => 0.0027089119 seconds
inpoly version
[segments_to_grid2] => 0.0001449585 seconds
168 times faster and equivalent output
Thanks Oliver!
Testing if a point lies inside a polygon is a well-known problem.
The classical solution is an algorithm called "ray casting":
Start from the point
Cast a ray in some arbitrary direction and count the number of times it crosses a polygon segment
The parity of the number gives the result (odd: inside; even: outside)
A C implementation of the algorithm is given here:
int pnpoly(int nvert, float *vertx, float *verty, float testx, float testy)
{
int i, j, c = 0;
for (i = 0, j = nvert-1; i < nvert; j = i++) {
if ( ((verty[i]>testy) != (verty[j]>testy)) &&
(testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) )
c = !c;
}
return c;
}
A possible PHP version is:
function pnpoly($nvert, $vertx, $verty, $testx, $testy) {
$c = false;
for ($i = 0, $j = $nvert - 1; $i < $nvert; $j = $i++) {
if ((($verty[$i] > $testy) != ($verty[$j] > $testy))
&& ($testx < ($vertx[$j] - $vertx[$i]) * ($testy - $verty[$i]) / ($verty[$j] - $verty[$i]) + $vertx[$i])) {
$c = !$c;
}
}
return $c;
}
Trying to convert the following PHP function to Python but getting the following error. What would be the working Python equivalent to the below PHP function?
line 140, in doDetectBigToSmall
for scale in xrange(start_scale, scale > 1,scale = scale* scale_update):
UnboundLocalError: local variable 'scale' referenced before assignment
PHP CODE:
protected function doDetectBigToSmall($ii, $ii2, $width, $height)
{
$s_w = $width/20.0;
$s_h = $height/20.0;
$start_scale = $s_h < $s_w ? $s_h : $s_w;
$scale_update = 1 / 1.2;
for ($scale = $start_scale; $scale > 1; $scale *= $scale_update) {
$w = (20*$scale) >> 0;
$endx = $width - $w - 1;
$endy = $height - $w - 1;
$step = max($scale, 2) >> 0;
$inv_area = 1 / ($w*$w);
for ($y = 0; $y < $endy; $y += $step) {
for ($x = 0; $x < $endx; $x += $step) {
$passed = $this->detectOnSubImage($x, $y, $scale, $ii, $ii2, $w, $width+1, $inv_area);
if ($passed) {
return array('x'=>$x, 'y'=>$y, 'w'=>$w);
}
} // end x
} // end y
} // end scale
return null;
}
PYTHON CODE:
def doDetectBigToSmall(self,ii, ii2, width, height):
s_w = width/20.0
s_h = height/20.0
start_scale = s_h if s_h < s_w else s_w
scale_update = 1 / 1.2
for scale in xrange(start_scale, scale > 1,scale = scale* scale_update):
w = (20*scale) >> 0
endx = width - w - 1
endy = height - w - 1
step = max(scale, 2) >> 0
inv_area = 1 / (w*w)
for y in xrange(0,y < endy,y = y + step):
for x in xrange(0, x < endx, x= x + step):
passed = self.detectOnSubImage(x, y, scale, ii, ii2, w, width+1, inv_area)
if (passed):
return {'x': x, 'y': y, 'w': w}
You have no idea what xrange() does ;-) So read the docs before you try it again. In the meantime, replace:
for scale in xrange(start_scale, scale > 1,scale = scale* scale_update):
with
scale = start_scale
while scale > 1:
and, at the end of the loop, add:
scale *= scale_update
All your other uses of xrange() are similarly broken, but you have to make some effort to learn what it does.
This works for me:
def strpos_r(haystack, needle):
positions = []
position = haystack.rfind(needle)
while position != -1:
positions.append(position)
haystack = haystack[:position]
position = haystack.rfind(needle)
return positions
Also, functions should not really handle input errors for you. You usually just return False or let the function throw an execution error.
This is happening because xrange is a function and you are passing an uninitialized value to it. Scale will not be initialized until after xrange is run and returns a value. Python generally used for loops to iterate over lists. I recommend rewriting your code using while loops.
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.
For a while now I've been interested in fractals, the math behind them and the visuals they can produce.
I just can't really figure out how to map the mathematical formula to a piece of code that draws the picture.
Given this formula for the mandelbrot set: Pc(z) = z * z + c
How does that compare to the following code:
$outer_adder = ($MaxIm - $MinIm) / $Lines;
$inner_adder = ($MaxRe - $MinRe) / $Cols;
for($Im = $MinIm; $Im <= $MaxIm; $Im += $outer_adder)
{
$x=0;
for($Re = $MinRe; $Re <= $MaxRe; $Re += $inner_adder)
{
$zr = $Re;
$zi = $Im;
for($n = 0; $n < $MaxIter; ++$n)
{
$a = $zr * $zr;
$b = $zi * $zi;
if($a + $b > 2) break;
$zi = 2 * $zr * $zi + $Im;
$zr = $a - $b + $Re;
}
$n = ($n >= $MaxIter ? $MaxIter - 1 : $n);
ImageFilledRectangle($img, $x, $y, $x, $y, $c[$n]);
++$x;
}
++$y;
}
Code is not complete, just showing the main iteration part for brevity.
So the question is: could someone explain to me how the math compares to the code?
Edit: To be clear, I've found dozens of resources explaining the math, and dozens of resources showing code, but nowhere can I find a good explanation of the two combined.
Disclaimer. I didn't know anything about fractals before, but always wanted to know, so I've read the wikipedia article and decided to write what I've found here.
As they say, if you want to understand something, try explaining it to someone else. ;)
Okay, we're going to operate on complex numbers. A complex number is actually a pair of (real) numbers, so, for us php programmers, let it be a two-elements array.
/// Construct a complex number from two reals
function cpl($re, $im) {
return array($re, $im);
}
Now we need to tell php how to do arithmetics on our complex numbers. We'll need addition, multiplication and the mod ("norm") operator. (see http://mathworld.wolfram.com/topics/ComplexNumbers.html for more details).
/// Add two complex numbers.
function cadd($p, $q) {
return cpl(
$p[0] + $q[0],
$p[1] + $q[1]);
}
/// Multiply two complex numbers.
function cmul($p, $q) {
return cpl(
$p[0] * $q[0] - $p[1] * $q[1],
$p[0] * $q[1] + $p[1] * $q[0]);
}
/// Return the norm of the complex number.
function cmod($p) {
return sqrt($p[0] * $p[0] + $p[1] * $p[1]);
}
Now we write a function that returns true if the given (complex) point $c belongs to the mandelbrot set
A point c belongs to the set if all points z = z^2 + c lie inside the circle with the radius 2.
We start with the complex number z = (0, 0).
On each step we calculate z = z * z + c.
If modulus of z > 2 - that is, we're out of the circle - the point is NOT in set
Otherwise repeat the step.
To prevent that from looping endlessly, limit the max number of iterations.
function is_in_mandelbrot_set($c, $iterations) {
$z = cpl(0, 0);
do {
if(cmod($z) >= 2)
return false;
$z = cadd(cmul($z, $z), $c);
} while($iterations--);
return true;
}
The rest has nothing to do with math and is quite obvious
function mandelbrot($img, $w, $h) {
$color = imagecolorallocate($img, 0xFF, 0, 0);
$zoom = 50;
$iters = 30;
for($x = 0; $x < $w; $x++) {
for($y = 0; $y < $h; $y++) {
// scale our integer points
// to be small real numbers around 0
$px = ($x - $w / 2) / $zoom;
$py = ($y - $h / 2) / $zoom;
$c = cpl($px, $py);
if(is_in_mandelbrot_set($c, $iters))
imagesetpixel($img, $x, $y, $color);
}
}
return $img;
}
$w = 200;
$h = 200;
header("Content-type: image/png");
imagepng(
mandelbrot(
imagecreatetruecolor($w, $h), $w, $h));
Result
Of course, this code is ineffective to the extreme. Its only purpose is to understand the math concept.