Search code examples
phpimage-processingphp-gd

single pixel manipulation with php GD


First I'm referring to a previous question Change image per pixel and save to db

I found that the html5 canvas isn't suitable because it's hard to keep the source-image secret. That's why I'm trying to achieve my goal with the PHP GD library. I never worked with this library so I have some difficulties. I guess I need the following functions

  • imagecreatetruecolor for creating the image in the browser
  • imagecolorallocate for returning rgb from source-image
  • imagesetpixel for drawing the random pixels

    $x = 200; //width of image
    $y = 200; //height of image
    
    $gd = imagecreatetruecolor ($x, $y);
     $color = imagecolorallocate($gd, $r, $g, $b) //not sure how to retrieve the rgb from the source image
    
    Then I need a function for drawing random pixels with imagesetpixel.
    imagesetpixel($gd, $posx,$posy, $color); // not sure how to retrieve the x and y position of each pixel.
    

I'm not a star with PHP, that's why my search is stuck with these GD functions. Hope you can give me a start-up


Solution

  • One of the "features" of the random function is that it's pseudorandom, i.e., it will always output the same sequence given the same seed.

    So you could store, for each "image":

    sourcefile   - the name of the source image
    seed         - integer, maybe the start time of this sequence
    position     - number of pixels that need to be shown, or maybe % completion
    

    So say that you want to output the image $sourcefile with seed $seed and $position percentage of pixels visible. You don't even need to use alpha:

    // Load image
    $src = imageCreateFromPNG($sourcefile); // Assume image is PNG
    
    // Create work image
    $new = imageCreateTrueColor(ImageSX($src), ImageSY($src)); // new image of same size
    
    mt_srand($seed); // Seed the Mersenne Twister generator
    
    // Number of pixels to set: $position = 0: NONE, $position = 100: ALL
    $pixels = round($position*imageSX($src)*imageSY($src)/100);
    
    // Now we have a problem: if we do $pixels attempts, mt_rand might sometimes
    // return the same pixel again. So we end up setting less than $pixels pixels.
    
    // So we do this the expensive way, saving an array of yet-to-be-used pixels.
    $max     = ImageSX($src)*ImageSY($src);
    $pixelid = array();
    for ($i = 0; $i < $max; $i++)
        $pixelid[] = $i;
    
    $W  = ImageSX($src);
    
    while($pixels--)
    {
        // Extract one pixel
        $chosen = $pixelid[$idx = mt_rand(0, $pixels)];
    
        array_splice ($pixelid, $idx, 1); // Remove extracted pixel from array
    
        $x = $chosen % $W;
        $y = ($chosen - $x)/ $W;
    
        $rgb = imagecolorat($src, $x, $y);
        $pix = imagecolorsforindex($src, $rgb);
        $rgb = imageColorAllocate($new, $pix['red'], $pix['green'], $pix['blue']);
        imageSetPixel($new, $x, $y, $rgb);
    }
    
    ImageDestroy($src);
    
    // $new has now exactly $pixels set to the same pixels of $src,
    // the rest are undefined (you can fill $new with white beforehand...)
    Header("Content-Type: image/png");
    ImagePNG($new);
    

    Variations

    Instead of splicing the array, you could blot out the "used" pixels, even if this does not give a uniform distribution:

    $cnt = count($pixelid);
    
    while($pixels--)
    {
        // Extract one pixel
        $idx = mt_rand(0, $cnt);
        // If the extracted pixel is null, find next pixel that is unextracted
        // and if there are none, restart from the beginning of the array.
        while (-1 == ($chosen = $pixelid[$idx]))
            if ($cnt == ++$idx)
                $idx = 0;
        $chosen = $pixelid[$idx];
        $pixelid[$idx] = -1;
    

    Or you could just... retry. But this can be expensive when the image is almost complete.

    $cnt = count($pixelid);
    while($pixels--)
    {
        // Extract one pixel
        for ($idx = mt_rand(0, $cnt); $pixelid[$idx] != -1; $idx = mt_rand(0, $cnt))
            ;
        $chosen = $pixelid[$idx];
        $pixelid[$idx] = -1;
    

    If you do not care that the image is rebuilt always in different manners, you can use array_shuffle() instead of mt_rand(), and at each iteration i extract the i-th pixel from $pixelid.

    A last option is to reimplement the array_shuffle() using mt_rand as detailed in the manual page (see example by tim at leethost dot com):

    function array_new_shuffle(&$items, $seed)
    {
        mt_srand($seed);
        for ($i = count($items) - 1; $i > 0; $i--)
        {
            $j = @mt_rand(0, $i);
            list($items[$i], $items[$j]) = array($items[$j], $items[$i]);
        }
    }
    

    So you would call array_new_shuffle() against $pixelid using $seed, and then extract the elements from the shuffled array in sequence:

    for ($idx = 0; $idx < $pixels; $idx++)
    {
        $chosen = $pixelid[$idx];
        ...
    

    Large Images

    For large images, handling the array is too expensive and you get out of memory. So, to avoid mt_rand() repeatedly hitting the same pixels (which can get really problematic when image is 99% complete, and therefore the probability of hitting at random one of the still viable 1% pixels is, of course, 1%), this hack uses another image as the index.

    This limits the "array" to 2^24 entries, that is, an image with a side of 2^12, or 4096 pixels.

    The memory savings are huge: each image pixel costs now 16 bytes, instead of around 176 (this on my Linux 64bit machine). This means that a 1024x1024 pixel image requires only about 17M of RAM.

    On my system, this script processes around 180k pixels per second (a 1024x1024 image was 100% processed in 7.4 seconds, of which around 2 were needed for image loading and setup).

    $seed = 0;
    $position = 2;
    $sourcefile = '/home/lserni/Lena19721024-filtered.png';
    
    mt_srand($seed); // Seed the Mersenne Twister generator
    
    // Load image
        $src = ImageCreateTrueColor(512,512);
    // $src = imageCreateFromPNG($sourcefile); // Assume image is PNG
    $W  = ImageSX($src);
    $H  = ImageSY($src);
    
    // Total number of pixels
    $size   = $W*$H;
    
    if (($W > 4095) || ($H > 4095))
       die("Image too big");
    
    // Create work image
    $new = imageCreateTrueColor($W, $H); // new image of same size
    
    /*
    if ($position > 50)
    {
        $position = 100-$position;
        $tmp = $src;
        $src = $new;
        $new = $tmp;
    }
    */
    
    // Number of pixels to set: $position = 0: NONE, $position = 100: ALL
    $pixels = round($position*$size/100.0);
    
    // Create a temporary buffer image of the same size
    $fix = imageCreateTrueColor($W, $H);
    for ($i = 0; $i < $size; $i++)
    {
        $b = $i & 0xFF;
        $g = ($i >> 8) & 0xFF;
        $r = ($i >> 16) & 0xFF;
        imageSetPixel($fix, $i % $W, floor($i / $W), imageColorAllocate($fix, $r, $g, $b));
    }
    
    while($pixels--)
    {
        // Recover one of the available pixel indexes
        $idx = mt_rand(0, $size--);
    
        // Recover index from image
        $y   = floor($idx / $W);
        $x   = $idx % $W;
        $idx = imageColorAt($fix, $x, $y);
        $lst = imageColorAt($fix, $size % $W, floor($size / $W));
        $b   = $lst & 0xff; $lst >>= 8;
        $g   = $lst & 0xff; $lst >>= 8;
        $r   = $lst & 0xff;
        imageSetPixel($fix, $x, $y, imageColorAllocate($fix, $r, $g, $b));
    
        // Whew. Now recover true x and y from new $idx
        $y   = floor($idx / $W);
        $x   = $idx % $W;
    
        $rgb = imagecolorat($src, $x, $y);
        $pix = imagecolorsforindex($src, $rgb);
        $rgb = imageColorAllocate($new, $pix['red'], $pix['green'], $pix['blue']);
        imageSetPixel($new, $x, $y, $rgb);
    }
    ImageDestroy($src);
    
    // $new has now exactly $pixels set to the same pixels of $src,
    // the rest are undefined (you can fill $new with white beforehand...)
    // die("Memory: " . memory_get_peak_usage());
    Header("Content-Type: image/png");
    ImagePNG($new);
    

    Optimization

    You'll notice a commented section in the above code. If it so happens that $position is more than 50%, say, 70%, it doesn't makes sense to create an empty image and copy 70% of pixels from the good image to the empty image. It makes more sense to copy 30% of empty pixels from the empty image to the good image, "blacking" it out. This can be accomplished by simply swapping $new and $src and adjusting $position. I haven't really thoroughly tested this code; that's why I left it commented. But you're welcome to give it a try.

    Implementation

    The advantage in using the PRNG is that you don't need to save any image, but only seed and position - typically eight bytes in all.

    If person A receives position 1, and asks to receive positions up to 5 (i.e. 5% of image visible), and you save the seed and this value of 5, and use it for person B, then person B will see the same 5% that person A got.

    All without any images being saved or loaded except the original one.

    If you can pass seed and position in the $_GET parameters, you can show in the browser the image at different stages (e.g. image.php?seed=12345678&position=5 would show the image with 5% of pixels set. You can also specify the number of pixels instead of their percentage, of course).

    This works as long as the pixels are chosen at random: if person A gets to choose the exact pixels he or she wants, then this approach is not valid and you need to save the individual pixel positions, which can be done in several ways: using a flatfile holding couples of (x,y) in binary format, or saving the whole image. The latter approach is easier to understand and needs storage for one whole image at each step, so if this is a game and you want to "replay" it, you might need huge disk space. The first approach might reasonably require six bytes per pixel, i.e. equivalent to an image which has the same height and is twice as wide as the original, or as few as four bytes per pixel.