skip to content

PHP: Extracting colours from an image

Here we're presenting a simple PHP class for sampling and averaging colours in a local or uploaded image.

Basic Approach

The ImageSampler class presented below takes as inputs:

$imagefile
path to a valid image file
$percent
the percentage of pixels to be sampled
$steps
the number of sections to be sampled and averaged will be $steps^2

In sampling the image we first partition it into a grid using the $steps value, and then sample $percent percent of the pixels in each partition, taking the average of the (RGB) colour values.

The resulting matrix is returned as a multi-dimensional array.

The ImageSampler class

Presenting the source code imagesampler.php:

<?PHP namespace Chirp; // Original PHP code by Chirp Internet: www.chirpinternet.eu // Please acknowledge use of this code by including this header. class ImageSampler { private $img; private $callback = NULL; private $initialized = FALSE; protected $percent = 5; protected $steps = 10; public $w, $h; public $sample_w = 0; public $sample_h = 0; public function __construct($imagefile) { if(!$this->img = imagecreatefromjpeg($imagefile)) { die("Error loading image: {$imagefile}"); } $this->w = imagesx($this->img); $this->h = imagesy($this->img); } public function set_percent($percent) { $percent = intval($percent); if(($percent < 1) || ($percent > 50)) { die("Your \$percent value needs to be between 1 and 50."); } $this->percent = $percent; } public function set_steps($steps) { $steps = intval($steps); if(($steps < 1) || ($steps > 50)) { die("Your \$steps value needs to be between 1 and 50."); } $this->steps = $steps; } private function set_callback($callback) { try { $fn = new \ReflectionFunction($callback); if($fn->getNumberOfParameters() != 4) { throw new \ReflectionException("Invalid parameter count in callback function. Usage: fn(int, int, int, bool) { ... }"); } $this->callback = $callback; } catch(\ReflectionException $e) { die($e->getMessage()); } } public function init() { $this->sample_w = $this->w / $this->steps; $this->sample_h = $this->h / $this->steps; $this->initialized = TRUE; } private function get_pixel_color($x, $y) { $rgb = imagecolorat($this->img, $x, $y); $r = ($rgb >> 16) & 0xFF; $g = ($rgb >> 8) & 0xFF; $b = $rgb & 0xFF; return [$r, $g, $b]; } public function sample($callback = NULL) { if(!$this->initialized) { $this->init(); } if(($this->sample_w < 2) || ($this->sample_h < 2)) { die("Your sampling size is too small for this image - reduce the \$steps value."); } if($callback) { $this->set_callback($callback); } $sample_size = round($this->sample_w * $this->sample_h * $this->percent / 100); $retval = []; for($i=0, $y=0; $i < $this->steps; $i++, $y += $this->sample_h) { $flag = FALSE; $row_retval = []; for($j=0, $x=0; $j < $this->steps; $j++, $x += $this->sample_w) { $total_r = $total_g = $total_b = 0; for($k=0; $k < $sample_size; $k++) { $pixel_x = $x + rand(0, $this->sample_w-1); $pixel_y = $y + rand(0, $this->sample_h-1); list($r, $g, $b) = $this->get_pixel_color($pixel_x, $pixel_y); $total_r += $r; $total_g += $g; $total_b += $b; } $avg_r = round($total_r/$sample_size); $avg_g = round($total_g/$sample_size); $avg_b = round($total_b/$sample_size); if($this->callback) { call_user_func_array($this->callback, [$avg_r, $avg_g, $avg_b, !$flag]); } $row_retval[] = [$avg_r, $avg_g, $avg_b]; $flag = TRUE; } $retval[] = $row_retval; } return $retval; } } ?>

expand code box

There is nothing complicated here, we just divide the image into a grid and haphazardly sample some pixels.

The most interesting part is the set_callback method which accepts a function as its argument and uses ReflectionClass to make sure the function accepts the correct number of arguments.

In PHP7 we can add type-hinting for scalar values (int, bool) which would allow a more detailed checking of the callback function.

Extracting colour values

The most basic usage is to just extract numeric colour values from an image. In this case we're using beach.jpg shown below:

<?PHP $sampler = new \Chirp\ImageSampler("beach.jpg"); $sampler->set_steps(2); $matrix = $sampler->sample(); ?>

This will populate the $matrix variable as a 2×2 array with each element being an array of R, G, B values, using the default sampling rate of 5%:

Array ( [ [106, 141, 171]
, [127, 158, 179]
], [ [ 40, 75, 75]
, [115, 124, 118]
] )

These are the average sampled colours of the four quadrants of the image. Below this will be clearer as we use the callback hooks to render the sampled values graphically.

Rendering colour samples with a callback

Of course we can always just take the array output (above), and run it through a loop, but where's the fun in that. Instead we're going to pass a callback function to the ImageSampler class to render the sampled colour directly.

<?PHP $sampler = new \Chirp\ImageSampler("beach.jpg"); $sampler->set_percent(10); $sampler->set_steps(2); $sampler->init(); ?> <style> .samples div { float: left; width: <?= $sampler->sample_w ?>px; height: <?= $sampler->sample_h ?>px; } </style> <div class="samples"> <?PHP $sampler_callback = function($r, $g, $b, $new_row) { echo "<div style=\""; if($new_row) { echo "clear: left; "; } echo "background: rgb($r,$g,$b);\"></div>\n"; }; $sampler->sample($sampler_callback); ?> </div> <div style="clear: both;"></div>

Note that after setting the percent and steps values we invoke the init() method in order to initialize the sample_* values for use in our CSS - so we can render the output at the original size.

As you can see, the output more or less matches what we had before:

As we increase the steps value, we get a more detailed result:

Looking more and more like the original:

If you set steps to 1 you will get an average colour for the entire image. And as you increase the percent value the result becomes more accurate, but at the same time more CPU-intensive. As you can see a sample rate of 10% is already quite good at matching the original.

What is it good for?

Here's an example where we take a 3×3 sample and use the output to create a nice CSS frame of gradients around the (any) image:

Source code:

<?PHP $sampler = new \Chirp\ImageSampler("beach.jpg"); $sampler->set_percent(10); $sampler->set_steps(3); $matrix = $sampler->sample(); ?> <div style="display: flex; flex-flow: row wrap; width: calc(<?= $sampler->w ?>px + 2em);"> <div style="flex: 0 0 100%; height: 1em; background: linear-gradient(to right, rgb(<?= implode(",", $matrix[0][0]) ?>), rgb(<?= implode(",", $matrix[0][1]) ?>), rgb(<?= implode(",", $matrix[0][2]) ?>));"></div> <div style="width: 1em; background: linear-gradient(to bottom, rgb(<?= implode(",", $matrix[0][0]) ?>), rgb(<?= implode(",", $matrix[1][0]) ?>), rgb(<?= implode(",", $matrix[2][0]) ?>));"></div> <div><img src="beach.jpg" alt=""></div> <div style="width: 1em; background: linear-gradient(to bottom, rgb(<?= implode(",", $matrix[0][2]) ?>), rgb(<?= implode(",", $matrix[1][2]) ?>), rgb(<?= implode(",", $matrix[2][2]) ?>));"></div> <div style="flex: 0 0 100%; height: 1em; background: linear-gradient(to right, rgb(<?= implode(",", $matrix[2][0]) ?>), rgb(<?= implode(",", $matrix[2][1]) ?>), rgb(<?= implode(",", $matrix[2][2]) ?>));"></div> </div>

Of course this particular effect can be achieved in the browser using just JavaScript, but that's another story.

Here we're extracting a spectrum of the nearest web safe colours from the image:

  1. #3366cc
  2. #669933
  3. #996666
  4. #663366
  5. #66cccc
  6. #000000
  7. #6699ff
  8. #cccccc
  9. #999966
  10. #333366
  11. #003366
  12. #006666
  13. #333300
  14. #003333
  15. #666699
  16. #99ccff
  17. #003300
  18. #336699
  19. #66ccff
  20. #cc9999
  21. #336633
  22. #669999
  23. #cccc99
  24. #3399cc
  25. #999999
  26. #666633
  27. #666666
  28. #6699cc
  29. #9999cc
  30. #336666
  31. #99cccc
  32. #333333
<?PHP function web_safe($val) { $retval = dechex(3 * round($val/51)); return "{$retval}{$retval}"; } $sampler = new \Chirp\ImageSampler("beach.jpg"); $sampler->set_steps(20); $matrix = $sampler->sample(); $tally = []; foreach($matrix as $row => $arr) { foreach($arr as $color) { list($r, $g, $b) = $color; $rgb = "#" . web_safe($r) . web_safe($g) . web_safe($b); if(!isset($tally[$rgb])) $tally[$rgb] = 0; $tally[$rgb]++; } } echo "<ol style=\"list-style-type: none; font-size: 0.9em; color: #666;\">\n"; asort($tally); foreach($tally as $rgb => $count) { echo " <li value=\"{$count}\"><div style=\"display: inline-block; width: {$count}em; height: 1em; background: {$rgb};\"></div> {$rgb}</li>\n"; } echo "</ol>\n\n"; ?>

As always, feel free to use and adapt this code and let us know if you have any questions or comments.

References

< PHP

User Comments

Post your comment or question

7 February, 2020

This is great. I wonder I have an image of a flat building with its surroundings and I'd like to measure out the amount of red-ish or yellow-ish color overlayed on the image. I've been trying to target the color using the charts you have in the end still the process eludes me.

top