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;
}
}
?>
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:
- #3366cc
- #669933
- #996666
- #663366
- #66cccc
- #000000
- #6699ff
- #cccccc
- #999966
- #333366
- #003366
- #006666
- #333300
- #003333
- #666699
- #99ccff
- #003300
- #336699
- #66ccff
- #cc9999
- #336633
- #669999
- #cccc99
- #3399cc
- #999999
- #666633
- #666666
- #6699cc
- #9999cc
- #336666
- #99cccc
- #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
- Stack Overflow: How to "validate" callback functions?
Related Articles - Image Manipulation
- PHP Extracting colours from an image
- PHP Creating images 'on the fly'
- PHP Effects of quality setting in JPEG compression
- System Adding a watermark using ImageMagick
Juan 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.