AI Foreground Color Selector
I’ve been wanting to use PHP-ML to solve a useful problem for a while, but was having trouble thinking of a case I might want to try first. Then it dawned on me that a Multi Layer Perceptron network should be reasonably good at picking text foreground colors, and I could quickly create training data for it.
So I did it, because I can. I opted to simply use black and white for this initial stab at the idea, but given enough training data, it should work with any set of arbitrary foreground colors.
Training Data
First step was producing the training data. This was simple enough, I’d just use a one-line-per-record format where and create it with a simple JavaScript interface. Ideally, we’d probably run through the full set of named CSS colors and maybe a few extra for outliers. However, I wanted to be done with my test as quickly as possible, so it simply produced random colors and had me pick black or white as the ideal foreground text color.
Ultimately, we had data that looked like this:
[240,240,240,"black"] [76,2,27,"white"] [251,104,78,"white"] [150,84,57,"white"] [30,139,210,"white"] [102,187,215,"black"] [118,238,238,"black"] [179,74,114,"white"] [94,141,37,"white"] [190,89,151,"white"] [118,70,194,"white"]
Training the machine learning algorithm
So, now we have our data, but there’s a bit of a problem. The sRGB color space model is not particularly representative of how humans perceive color. Enter CIELab. So the next step in preparing the data for training was to convert from sRGB to CIELab and then normalize those values for use as inputs for our samples. Instead of writing that myself, I used the ColorJizz-PHP library.
use MischiefCollective\ColorJizz\Formats\RGB; function rgb_to_neuron_input($r, $g, $b) { $color = new RGB($r, $g, $b); $lab = $color->toCIELab(); //Normalize to 0-1 for each component. return [ $lab->lightness / 100, ($lab->a_dimension + 128) / 256, ($lab->b_dimension + 128) / 256 ]; }
I didn’t do much configuration of the MLP network. I only added a few hidden layers and a handful of nodes. That was enough to make the training process slow. Taking about 45 seconds to train against 140 samples.
Results:
The results actually weren’t terrible. I went back and added a few more training samples for some cases where it was really wrong, but overall it made sane selections.
As an added bonus, the MultiLayerPerceptron class is serializable. So I could upload PHP-ML’s trained models to just about any shared hosting provider and make use of them for generating predictions.
Here are some of the results:
Test 1
BG: rgb(151, 109, 87); foreground: white |
BG: rgb(10, 126, 134); foreground: white |
BG: rgb(6, 143, 62); foreground: white |
BG: rgb(129, 234, 222); foreground: black |
BG: rgb(173, 143, 183); foreground: white |
BG: rgb(159, 120, 6); foreground: white |
BG: rgb(111, 53, 155); foreground: white |
BG: rgb(78, 254, 109); foreground: black |
BG: rgb(140, 227, 137); foreground: black |
BG: rgb(217, 215, 238); foreground: black |
BG: rgb(97, 166, 46); foreground: white |
BG: rgb(90, 18, 82); foreground: white |
BG: rgb(170, 101, 173); foreground: white |
BG: rgb(147, 196, 162); foreground: black |
BG: rgb(117, 223, 206); foreground: black |
BG: rgb(142, 144, 42); foreground: white |
BG: rgb(186, 67, 141); foreground: white |
BG: rgb(156, 241, 150); foreground: black |
BG: rgb(129, 11, 151); foreground: white |
BG: rgb(208, 241, 232); foreground: black |
BG: rgb(87, 79, 179); foreground: white |
BG: rgb(156, 7, 78); foreground: white |
BG: rgb(18, 46, 228); foreground: white |
BG: rgb(121, 91, 251); foreground: white |
BG: rgb(58, 187, 45); foreground: black |
BG: rgb(90, 177, 111); foreground: black |
BG: rgb(244, 161, 163); foreground: black |
BG: rgb(14, 101, 17); foreground: white |
BG: rgb(93, 129, 211); foreground: white |
BG: rgb(171, 103, 104); foreground: white |
BG: rgb(54, 33, 33); foreground: white |
BG: rgb(221, 71, 237); foreground: white |
BG: rgb(136, 250, 216); foreground: black |
BG: rgb(35, 3, 197); foreground: white |
BG: rgb(233, 1, 146); foreground: white |
BG: rgb(37, 33, 182); foreground: white |
BG: rgb(194, 39, 30); foreground: white |
BG: rgb(11, 233, 219); foreground: black |
BG: rgb(221, 23, 192); foreground: white |
BG: rgb(181, 228, 178); foreground: black |
Test 2
BG: rgb(220, 251, 249); foreground: black |
BG: rgb(72, 241, 170); foreground: black |
BG: rgb(161, 118, 4); foreground: black |
BG: rgb(201, 139, 30); foreground: black |
BG: rgb(69, 17, 8); foreground: white |
BG: rgb(37, 36, 234); foreground: white |
BG: rgb(12, 17, 150); foreground: white |
BG: rgb(59, 144, 239); foreground: black |
BG: rgb(71, 107, 240); foreground: white |
BG: rgb(158, 238, 229); foreground: black |
BG: rgb(189, 171, 29); foreground: black |
BG: rgb(86, 142, 219); foreground: black |
BG: rgb(226, 189, 246); foreground: black |
BG: rgb(242, 71, 122); foreground: black |
BG: rgb(189, 29, 196); foreground: white |
BG: rgb(156, 137, 171); foreground: black |
BG: rgb(149, 205, 0); foreground: black |
BG: rgb(100, 234, 155); foreground: black |
BG: rgb(155, 128, 195); foreground: black |
BG: rgb(228, 42, 195); foreground: white |
BG: rgb(58, 0, 20); foreground: white |
BG: rgb(165, 161, 190); foreground: black |
BG: rgb(196, 253, 239); foreground: black |
BG: rgb(110, 110, 208); foreground: white |
BG: rgb(174, 218, 82); foreground: black |
BG: rgb(83, 203, 221); foreground: black |
BG: rgb(65, 219, 223); foreground: black |
BG: rgb(91, 101, 121); foreground: white |
BG: rgb(184, 203, 194); foreground: black |
BG: rgb(152, 93, 172); foreground: white |
BG: rgb(202, 83, 132); foreground: white |
BG: rgb(20, 172, 245); foreground: black |
BG: rgb(134, 104, 124); foreground: white |
BG: rgb(95, 73, 167); foreground: white |
BG: rgb(124, 172, 83); foreground: black |
BG: rgb(14, 161, 102); foreground: black |
BG: rgb(36, 150, 117); foreground: black |
BG: rgb(10, 248, 104); foreground: black |
BG: rgb(184, 148, 197); foreground: black |
BG: rgb(205, 138, 31); foreground: black |