Image fun

September 14th, 2005. Tagged: PEAR

Here are some examples of image manipulation using the GD library, more specifically these are pixel operations. Pixel operation meaning doing something to a pixel in an image with regards to this pixel only, not taking into account the neighbours.

An example pixel operation is making a negative image. You take every pixel in an image and substitute it with its opposite color pixel.

OK, so how is this thing working. Pretty simple. I take a PNG image, go through every pixel of this image and call a function passing the pixel as a parameter. The callback function returns a new pixel. I take all returned pixels and create a new image.

Pixel class

To get started I have a pixel class. It simply contains three integer values, the values of red, green and blue the pixel has.

<?php
class Pixel {
    function Pixel($r, $g, $b)
    {
        $this->r = ($r > 255) ? 255 : (($r < 0) ? 0 : (int)($r));
        $this->g = ($g > 255) ? 255 : (($g < 0) ? 0 : (int)($g));
        $this->b = ($b > 255) ? 255 : (($b < 0) ? 0 : (int)($b));
    }
}
?>

This class has only one method, the constructor of the class, which takes care to sanitize the RGB values.

To create a red pixel, you simply do:

<?php
$red = new Pixel(255, 0, 0);
?>

The pixel operations class and main method

Then I have a class that will do the actual operations, I call the Image_PixelOperations. I didn't go through making a nice interface for reading and writing different file formats, I was thinking that this class can be developed further and based on PEAR's Image_Transform, which has tools for opening, validating, displaying, writing image files. What I needed the most was a simple method that opens a PNG, goes through every pixel, calls a function, gets a new pixel and writes the pixel to a new image. Hence, the pixelOperation method:

<?php
class Image_PixelOperations {

    function pixelOperation(
            $input_image,
            $output_image,
            $operation_callback,
            $factor = false
            )
    {

        $image = imagecreatefrompng($input_image);
        $x_dimension = imagesx($image);
        $y_dimension = imagesy($image);
        $new_image = imagecreatetruecolor($x_dimension, $y_dimension);

        if ($operation_callback == 'contrast') {
            $average_luminance = $this->getAverageLuminance($image);
        } else {
            $average_luminance = false;
        }

        for ($x = 0; $x < $x_dimension; $x++) {
            for ($y = 0; $y < $y_dimension; $y++) {

                $rgb = imagecolorat($image, $x, $y);
                $r = ($rgb >> 16) & 0xFF;
                $g = ($rgb >> 8) & 0xFF;
                $b = $rgb & 0xFF;

                $pixel = new Pixel($r, $g, $b);
                $pixel = call_user_func(
                    $operation_callback,
                    $pixel,
                    $factor,
                    $average_luminance
                );

                $color = imagecolorallocate(
                    $image,
                    $pixel->r,
                    $pixel->g,
                    $pixel->b
                );
                imagesetpixel($new_image, $x, $y, $color);
            }

        }

        imagepng($new_image, $output_image);
    }
}

?>

The method takes a filename, it is not doing any validation, it assumes it's a valid PNG file. The second parameter is the output filename. The third is the callback function that will be called on every pixel and the last parameter is any additional parameter we want to pass to the callback function.
The average luminance is something specific to the "contrast" operation, so it's no too important for what pixelOrerations does.

Adding noise

Time for writing the first callback, the addNoise method. Adding noise to an image means adding a random value to each channel of a pixel. (If you're wondering, the value of red in a pixel is called a channel, and so are the blue and the green). Here goes the addNoise implementation.

<?php
    function addNoise($pixel, $factor)
    {
        $random = mt_rand(-$factor, $factor);
        return new Pixel(
                    $pixel->r + $random,
                    $pixel->g + $random,
                    $pixel->b + $random
                );
    }
?>

What we have here is generation of a random value in a user-specified range and adding the random value to the pixel. User-specified range is a number between 0 and 255, where 0 means less noise and 255 means more noise. Well, 0 means no noise and 255 means a lot of noise, 255 is not a boundary, you can go above and the more you go the more you get noise only and pretty much nothing of the original image.

Let's test! I have the simplest of HTML forms:

<form method="get">
    <input name="image" />
    <input type="submit" />
</form>

I specify the image name in the form and submit.

Then if something was submitted I create an object on the pixel operation class:

<?php
if (!empty($_GET['image'])) {

    $po =& new Image_PixelOperations();

}
?>

Then I display the original image, call the pixelOperation method and display the result:

<?php
    echo 'Original: <br /><img src="'. $_GET['image'] .'" />';
    echo '<hr />';
    
    // noise
    $noise = 100;
    $po->pixelOperation($_GET['image'], 'result_noise.png', array($po, 'addNoise'), $noise);
    echo '<br />Add noise (factor '. $noise .'): <br /><img src="result_noise.png" />';
    echo '<hr />';

?>

The result is:

Adding more or less noise gives the following results where the first one has a noise factor of 20 and the second one, 500:

and

Brightness

Next, adjusting brightness. This is nothing but adding the same integer to each channel of each and every pixel. If we add, we brighten the image, if we substract, we darken the image. The callback method is:

<?php
    function adjustBrightness($pixel, $factor)
    {

        return new Pixel(
                    $pixel->r + $factor,
                    $pixel->g + $factor,
                    $pixel->b + $factor
                );
    }
?>

To test this, we do:

<?php
    $brightness = 50;
    $po->pixelOperation($_GET['image'], 'result_bright.png', array($po, 'adjustBrightness'), $brightness);
    echo '<br />Brighten: <br /><img src="result_bright.png" />';
    $brightness = -50;
    $po->pixelOperation($_GET['image'], 'result_dark.png', array($po, 'adjustBrightness'), $brightness);
    echo '<br />Darken: <br /><img src="result_dark.png" />';
    echo '<hr />';
?>

Which gives us:

and

Swap colors

Next, colors swapping. This means for example take the amount of red in a pixel and replace it with the amount of blue in the same pixel. So there are these possibilities for swapping:

  • RGB to RBG
  • RGB to BGR
  • RGB to BRG
  • RGB to GBR
  • RGB to GRB

The method definition is pretty simple:

<?php
    function swapColors($pixel, $factor)
    {

        switch ($factor) {
            case 'rbg':
                return new Pixel(
                            $pixel->r,
                            $pixel->b,
                            $pixel->g
                        );
                break;
            case 'bgr':
                return new Pixel(
                            $pixel->b,
                            $pixel->g,
                            $pixel->r
                        );
                break;
            case 'brg':
                return new Pixel(
                            $pixel->b,
                            $pixel->r,
                            $pixel->g
                        );
                break;        
            case 'gbr':
                return new Pixel(
                            $pixel->g,
                            $pixel->b,
                            $pixel->r
                        );
                break;        
            case 'grb':
                return new Pixel(
                            $pixel->g,
                            $pixel->r,
                            $pixel->b
                        );
                break;        
            default:
                return $pixel;
        }

    }

?>

Testing the different possibilities gives us:
RGB to RBG:

RGB to BGR:

RGB to BRG:

RGB to GBR:

RGB to GRB:

Removing and boosting colors

Next, we have two more methods. One that sets a channel to zero (for example "no red!") the other one maximizes a channel. Or two channels. So we have six options for each method:

  • Removing (or maximizing) red
  • Removing (or maximizing) green
  • Removing (or maximizing) blue
  • Removing (or maximizing) red and green at the same time
  • Removing (or maximizing) red and blue
  • Removing (or maximizing) green and blue

(It doesn't make much sense to remove all three channels or to maximize them. Why?)

The implementation:

<?php
    function removeColor($pixel, $factor)
    {

        if ($factor == 'r' ) {
            $pixel->r = 0;
        }
        if ($factor == 'g' ) {
            $pixel->g = 0;
        }
        if ($factor == 'b' ) {
            $pixel->b = 0;
        }
        if ($factor == 'rb' || $factor == 'br') {
            $pixel->r = 0;
            $pixel->b = 0;
        }
        if ($factor == 'rg' || $factor == 'gr') {
            $pixel->r = 0;
            $pixel->g = 0;
        }
        if ($factor == 'bg' || $factor == 'gb') {
            $pixel->b = 0;
            $pixel->g = 0;
        }

        return $pixel;
    }

    function maxColor($pixel, $factor)
    {

        if ($factor == 'r' ) {
            $pixel->r = 255;
        }
        if ($factor == 'g' ) {
            $pixel->g = 255;
        }
        if ($factor == 'b' ) {
            $pixel->b = 255;
        }
        if ($factor == 'rb' || $factor == 'br') {
            $pixel->r = 255;
            $pixel->b = 255;
        }
        if ($factor == 'rg' || $factor == 'gr') {
            $pixel->r = 255;
            $pixel->g = 255;
        }
        if ($factor == 'bg' || $factor == 'gb') {
            $pixel->b = 255;
            $pixel->g = 255;
        }

        return $pixel;
    }

?>

And the tests:
Remove red:

Remove green:

Remove blue:

Remove red and green:

Remove green and blue:

Remove red and blue:

Maximize red:

Maximize green:

Maximize blue:

Maximize red and green:

Maximize green and blue:

Maximize red and blue:

Negative

This one is pretty easy, negate the channel. The logic is - you have a lot of red? Nah, I'll use the opposite, less red.

<?php
    function negative($pixel)
    {
        return new Pixel(
                    255 - $pixel->g,
                    255 - $pixel->r,
                    255 - $pixel->b
                );
    }
?>

The test gives us:

Greyscale

I don't know if you know it, but the gray is a color that has equal amounts of R, G and B. Darker shades of gray have a log of R, G and B, the lighter shades have less. To greyscale an image we take the average of the amounts of R, G and B and set the three channels to the average.

<?php
    function greyscale($pixel)
    {

        $pixel_average = ($pixel->r + $pixel->g + $pixel->b) / 3;

        return new Pixel(
                    $pixel_average,
                    $pixel_average,
                    $pixel_average
                );
    }
?>

Test:

Black and White

Unlike greyscale that has shades, B&W has only two colors black (0, 0, 0) and white (255, 255, 255). We use a factor here to determine where the boundary would be, meaning what do you consider black and what white. The simplest logic is that we sum R+G+B and if it's closer to 255+255+255 than it is to 0 (0+0+0), then we call it white, otherwise it is black. Using a factor gives us some more flexibility of drawing the line between black and white (This reflects the subjectivity of real life ;) )

<?php
    function blackAndWhite($pixel, $factor)
    {
        $pixel_total = ($pixel->r + $pixel->g + $pixel->b);

        if ($pixel_total > (((255 + $factor) / 2) * 3)) {
            // white
            $pixel->r = 255;
            $pixel->g = 255;
            $pixel->b = 255;
        } else {
            $pixel->r = 0;
            $pixel->g = 0;
            $pixel->b = 0;
        }

        return $pixel;
    }

?>

Test with factor 20:

Clip

At this point I started hunting the web for ideas for more pixel manipulations (I'm still open, BTW, post a comment with anything you find). I found this clipping thing described somewhere. I'm not sure how useful it is (maybe I didn't get it right). It is in essence removing boundary values and replacing them with pure 0 or 255. So you have (5, 155, 250), this will become (0, 155, 255). Again, there is a factor to give you flexibility in drawing the line. I'm not convinced how useful this is, the only thing I can think of is decreased file size, because the new image uses less colors. Anyway, here's the implementation and the test.

<?php
    function clip($pixel, $factor)
    {
        if ($pixel->r > 255 - $factor) {
            $pixel->r = 255;
        }
        if ($pixel->r < $factor) {
            $pixel->r = 0;
        }
        if ($pixel->g > 255 - $factor) {
            $pixel->g = 255;
        }
        if ($pixel->g < $factor) {
            $pixel->g = 0;
        }
        if ($pixel->b > 255 - $factor) {
            $pixel->b = 255;
        }
        if ($pixel->b < $factor) {
            $pixel->b = 0;
        }

        return $pixel;
    }
?>

Clipping with factor 100:

Adjusting contrast

This is not a pure pixel operation because it takes into account all the pixels in an image in order to decide how to manipulate a given pixel. The contrast adjustment needs the so called average luminance. In order to calculate the average luminance, you need a formula, which I copied, so the implementation is:

<?php
    function getAverageLuminance($image)
    {

        $luminance_running_sum = 0;
        
        $x_dimension = imagesx($image);
        $y_dimension = imagesy($image);

        for ($x = 0; $x < $x_dimension; $x++) {
            for ($y = 0; $y < $y_dimension; $y++) {

                $rgb = imagecolorat($image, $x, $y);
                $r = ($rgb >> 16) & 0xFF;
                $g = ($rgb >> 8) & 0xFF;
                $b = $rgb & 0xFF;

                $luminance_running_sum += (0.30 * $r) + (0.59 * $g) + (0.11 * $b);
                
            }

        }
       
        $total_pixels = $x_dimension * $y_dimension;

        return $luminance_running_sum / $total_pixels;
    }

?>

The actual contrast callback is simple:

<?php
    function contrast($pixel, $factor, $average_luminance)
    {
        
        return new Pixel(
            $pixel->r * $factor + (1 - $factor) * $average_luminance,
            $pixel->g * $factor + (1 - $factor) * $average_luminance,
            $pixel->b * $factor + (1 - $factor) * $average_luminance
            );
    }
?>

Tests with decreased and increased contrast (factors 0.5 and 1.5):

Salt and Pepper

I got the idea from this page. It's basically sprinkling random white (salt) and black (pepper) pixels. The implementation:

<?php
    function saltAndPepper($pixel, $factor)
    {
        
        $black = (int)($factor/2 + 1);
        $white = (int)($factor/2 - 1);
        
        $random = mt_rand(0, $factor);
        
        $new_channel = false;
        
        if ($random == $black) {
            $new_channel = 0;
        }
        if ($random == $white) {
            $new_channel = 255;
        }
        
        if (is_int($new_channel)) {
        
            return new Pixel($new_channel, $new_channel, $new_channel);
            
        } else {
            return $pixel;
        }
    }
?>

Test with factor 20:

Gamma correction

<?php
    function gamma($pixel, $factor)
    {
        
        return new Pixel(
                pow($pixel->r / 255, $factor) * 255,
                pow($pixel->g / 255, $factor) * 255,
                pow($pixel->b / 255, $factor) * 255
            );
    }
?>

Test with factor 2.2:

Randomize

This is just picking a random function from the ones described above a calling it with a random factor. The result? Well, a scientific way to create worst (or better, it's all how you look at it) colorful noise ;)

Next?

I thought of two functions that might be useful. One of them is to snap a pixel to a predefined color. I mean if a pixel is close to something we want, make it closer. This might be useful when for example you want to change the color scheme of your website and you want to alter all images to make them pinkier for example. So far the experiments are going well, but the results ate not too promising ;)

The other function would be to replace a color with another one, while also using proximity. For example take all very very light shades of gray and make them white.

Then there are the filters to do, another set of operations which take into account not only the current pixel but also the neighbours. Examples - blur, edge detect, etc.

BTW, the full source for this posting is here. Extending it is easy, you just need to define a callback function for a new effect and pass it in a call to pixelOperation. I'll be happy if you post the results here.

-----------------

Update form 2005-09-21:

Thanks to Laurens Holst who posted this comment about the sensitivity of the human eye to the different colors! So I created a new greyscaling function using the formula he suggested. In my original function I just take the average of the R, G and B, while now these colors come each with a coeficient. Then I divide by the sum of the coeficients.

The result is better!

Here's the experiment with the same Wiki image (the second greyscale image is using the new formula)

The new image looks just a bit brighter, nothing special. But I tried with another image I found on SitePoint and it looks like the second image is not only brighter but contains more human-eye-readable information. Take a lcoser look at what looks like a display on a cash register. The second greyscale image looks more "informative".

Here's the source of the new greyscale function:

<?php
function greyscale2($pixel) 
{

    $pixel_average = 
        ( 0.3  * $pixel->r 
        + 0.59 * $pixel->g 
        + 0.11 * $pixel->b) / (0.3 + 0.59 + 0.11);

    return new Pixel(
                $pixel_average,
                $pixel_average,
                $pixel_average 
            );

}
?>

Tell your friends about this post: Facebook, Twitter, Google+

Sorry, comments disabled and hidden due to excessive spam. Working on restoring the existing comments...

Meanwhile, hit me up on twitter @stoyanstefanov