The last time I looked at sorting colors I had produced a nice band or sorted colors, but to do so I had essentially removed a third of the data from the color information. This meant that there was no white or black colors in the band of sorted colors.
After a bit of thinking on how to solve this I hit upon a way of using a two dimensional array of colors to filter the colors into blocks. This would allow the missing color information to be rendered correctly, and would only mean a small amount of work to allow it to work with the rendering function used in the previous examples.
Generating The Data
I could easily just generate every color available and use that as the data. What I wanted to generate was a random assortment of colors that would represent the sort of data being produced by a system or other input.
To generate the data I created an array of 360 blank arrays. The 360 represents the hue value of the color so all that was needed is to create the saturation and values for the color and then store them in the correct array.
The following code initialises the color arrays and then fills them with some random data. The hsvToRgb() function can be found in part 2.
$colorRanges = array_fill(0, 360, []);
foreach ($colorRanges as $hue => $range) {
for ($i = 0; $i < 300; ++$i) {
// Generate random values for saturation and value.
$saturation = mt_rand(0, 100) / 100;
$value = mt_rand(0, 100) / 100;
// Convert the color from hsv to rgb.
$rgb = hsvToRbg($hue, $saturation, $value);
// Create Color object.
$colorRanges[$hue][] = new Color($rgb['red'], $rgb['blue'], $rgb['green']);
}
}
The loop of 300 is just an arbitrary value to generate a decent amount of data in each hue array.
Rendering
The rendering function just needs a little bit of alteration so that it can render a multidimensional array of Color objects instead of just a flat array.
function renderColors($colorRanges, $sortedBy) {
$pixelSize = 1;
// Work out width.
$width = count($colorRanges) * $pixelSize;
// Ensure the correct height is worked out.
$height = 0;
foreach ($colorRanges as $colorRange) {
$potentialHeight = count($colorRange) * $pixelSize;
if ($potentialHeight > $height) {
$height = $potentialHeight;
}
}
// Initialize image.
$im = imagecreatetruecolor($width, $height);
// Fill image with colored pixels.
$x = 0;
foreach ($colorRanges as $colorRange) {
$y = 0;
foreach ($colorRange as $yColor => $color) {
$pixelColour = imagecolorallocate($im, $color->red, $color->green, $color->blue);
imagefilledrectangle($im, $x, $y, $x + $pixelSize, $y + $pixelSize, $pixelColour);
$y = $y + $pixelSize;
}
$x = $x + $pixelSize;
}
// Save image.
imagepng($im, 'colors-' . $sortedBy . '.png');
imagedestroy($im);
}
Without sorting this generates the following image.
This shows that the hue is already sorted into bands of colors, so all we need to do is go through each band and sort it by other values.
Sorting
With that in place we need to sort each of the arrays and see if we can get any decent results by sorting just the bands of colors. This differs from the sorting done in the other posts, with the difference that we loop through the array and sort each band of colors.
RGB
Sorting by RGB is just as it sounds, just add together the reg, green and blue values and compare them.
foreach ($colorRanges as $id => $colorRange) {
usort($colorRange, function ($a, $b) {
return ($a->red + $a->green + $a->blue) <=> ($b->red + $b->green + $b->blue);
});
$colorRanges[$id] = $colorRange;
}
renderColors($colorRanges, 'rgb' . time());
This generates the following image.
It's clear that some sorting has taken place here. The whiter colors are pushed towards the bottom and the darker colors are pushed towards the top. Grey is still a bit of a problem as it doesn't appear to have moved much.
Lightness
For lightness I am re-using the calculateLightness() function from part 1.
foreach ($colorRanges as $id => $colorRange) {
usort($colorRange, function ($a, $b) {
return calculateLightness($a) <=> calculateLightness($b);
});
$colorRanges[$id] = $colorRange;
}
renderColors($colorRanges, 'lightness' . time());
This produces the following output.
Similar to the RGB sorting this works for lighter and darker colors, but greys are largely ignored. I thought this would create better results than just adding together the RGB values but this doesn't appear to be the case.
Luminance
The luminance of the color is calculated by working out the proportions of red, green and blue to create a perceived level of lightness. Re-using the calculateLuminance() function from part 1.
foreach ($colorRanges as $id => $colorRange) {
usort($colorRange, function ($a, $b) {
return calculateLuminance($a) <=> calculateLuminance($b);
});
$colorRanges[$id] = $colorRange;
}
renderColors($colorRanges, 'luminance' . time());
This produces the following image.
This creates a better result than lightness or RGB sorting, but grey is still causing problems.
HSV
Sorting by hue, saturation and value is not necessary as we have technically already sorted by hue. As a result we just need to sort by the sum of saturation and value.
foreach ($colorRanges as $id => $colorRange) {
usort($colorRange, function ($a, $b) {
$hsv1 = rgbTohsv($a);
$hsv2 = rgbTohsv($b);
return ($hsv1['saturation'] + $hsv1['value']) <=> ($hsv2['saturation'] + $hsv2['value']);
});
$colorRanges[$id] = $colorRange;
}
renderColors($colorRanges, 'hsv' . time());
This creates the following image.
This has pushed the high saturation colors to the bottom and the low saturation colors to the top. This results in the whites and grey colors being distributed across the spectrum and not sorted into the correct result.
HEX
Out of interest (and because HEX sorting is quite good when sorting with single bands of colors) I decided to try sorting by the HEX value of the color.
foreach ($colorRanges as $id => $colorRange) {
usort($colorRange, function ($a, $b) {
$aValue['red'] = str_pad(dechex($a->red), 2, '0', STR_PAD_LEFT);
$aValue['green'] = str_pad(dechex($a->green), 2, '0', STR_PAD_LEFT);
$aValue['blue'] = str_pad(dechex($a->blue), 2, '0', STR_PAD_LEFT);
$bValue['red'] = str_pad(dechex($b->red), 2, '0', STR_PAD_LEFT);
$bValue['green'] = str_pad(dechex($b->green), 2, '0', STR_PAD_LEFT);
$bValue['blue'] = str_pad(dechex($b->blue), 2, '0', STR_PAD_LEFT);
$aValue = implode($aValue);
$bValue = implode($bValue);
return $aValue <=> $bValue;
});
$colorRanges[$id] = $colorRange;
}
renderColors($colorRanges, 'hex' . time());
This generates the following image.
Yup, pretty terrible. Weirdly, this reminds me of the color absorption spectrum of chlorophyll. This pattern is created because of the way that the hex value is created. Because of the way in which the hex value is added together I have essentially created a process that gives more precedence to red colors so those colors are slightly better sorted in this example.
Not Enough?
This approach of sorting a multidimensional array is closer than sorting a single dimensional array, but there are still a few problems. I stated before that creating bands of hue creates separate blocks of colors. Although this is technically true we still can't sort because we essentially have to combine one or more values in order to generate a sorting algorithm. Although we can separate and sort each color there are still a lot of grey values that don't fit nicely into the sorting algorithm because we are essentially trying to force a three-dimensional data item (i.e. the color) into a two-dimensional array.
Add new comment