Basic Color Masking with PHP

There was a certain set of tools in Photoshop that I really enjoyed using back in my days of graphic design. Whether it was the color match, the magic wand, or some sort of filtering mask, the ability to pull objects out of a flat image seemed pretty cool. And, thanks to some of the different image manipulation libraries in PHP, not too difficult to replicate.

After digging around a bit I sketched out a few different approaches I could take. There's a color angle - usually objects in a photo have a certain grouping of colors (or the background does). If I wanted to pull a face out of a photo I could hunt down flesh-colored pixels. If I wanted to pull an entire person (face, hair, clothes, etc) than that would be more difficult and I'd probably try to select and discard the background instead.

There are other parameters, like pixel proximity, approximate lines/shapes, shadows and hues, etc, but the color angle seemed to be one of the easiest to start with. By leaning on the GD library in PHP you can easily target a pixel in a JPEG, pull its color, map the value to a standard format (rgb), and then do a test to see if it fits within the masking range. The code to do this wasn't terribly difficult, though there were a few interesting points I ran into along the way.

  1. /**

  2. * First, let's declare some variables

  3. */

  4. // the mask color

  5. $mask = array(

  6. 'red' => 0x00,

  7. 'green' => 0xFF,

  8. 'blue' => 0x00,

  9. );

  10. // background color to replace

  11. $target_color = array(

  12. 'red' => 0x70,

  13. 'green' => 0xA0,

  14. 'blue' => 0xF6,

  15. );

  16. // tolerance for color matching

  17. $tolerance = .2;

  18. // path of the image to pull from

  19. $input_path = 'yay.jpg';

  20. // path of the image to output

  21. $output_path = 'boo.jpg';

  22. // bump up the memory limit for processing

  23. ini_set('memory_limit', '512M');

  24. /**

  25. * Helper functions

  26. */

  27. function transform_to_rgb($color) {

  28. return array(

  29. 'red' => ($color >> 16) & 0xFF,

  30. 'green' => ($color >> 8) & 0xFF,

  31. 'blue' => $color & 0xFF,

  32. );

  33. }

  34. function compute_color_similarity($color_one, $color_two) {

  35. return sqrt(

  36. pow(($color_two['red'] - $color_one['red']), 2) +

  37. pow(($color_two['green'] - $color_one['green']), 2) +

  38. pow(($color_two['blue'] - $color_one['blue']), 2)) /

  39. sqrt(pow(255, 2) + pow(255, 2) + pow(255, 2));

  40. }

  41. /**

  42. * Now it's time for the processing

  43. */

  44. $input = imagecreatefromjpeg($input_path);

  45. $width = imagesx($input);

  46. $height = imagesy($input);

  47. $output = imagecreatetruecolor($width, $height);

  48. $mask_color = imagecolorallocate($output, $mask['red'], $mask['green'], $mask['blue']);

  49. for ($y = 0; $y < $height; $y++) {

  50. for ($x = 0; $x < $width; $x++) {

  51. $pixel_color = imagecolorat($input, $x, $y);

  52. $pixel_color = transform_to_rgb($pixel_color);

  53. $similarity = compute_color_similarity($pixel_color, $target_color);

  54. if ($similarity <= $tolerance) {

  55. $replace_color = $mask_color;

  56. } else {

  57. $replace_color = imagecolorallocate($output, $pixel_color['red'], $pixel_color['green'], $pixel_color['blue']);

  58. }

  59. imagesetpixel($output, $x, $y, $replace_color);

  60. }

  61. }

  62. imagedestroy($input);

  63. imagejpeg($output, $output_path);

  64. imagedestroy($output);

Comparing colors isn't as easy as it sounds. I browsed around the internet trying to find a good method and ended up with a dozen. Some people claim that HSL values are easier to compare than RGB, others promote using complex algorithms, while some just prefer to use a mixture of multiple methods. Since the range of colors that I'm looking for is relatively small (hopefully) I wasn't too worried about accurate comparisons of very different colors. I just settled on a typical three-dimensional distance formula instead.

When doing the actual mask I found it easiest to create a blank image and plop each individual pixel down in place. I had hoped to do it more streaming, opening up a connection with the input, iterating through each pixel, and then changing it if necessary, but that isn't really an option using PHP GD. It may have something to do with JPEG compression (thus, it may be possible to stream with TIFFs). Whatever the reason is it forced me to bump my memory usage to an embarrassing level.

Another interesting point is the color -> RGB conversion. As a web designer I've mostly dealt with colors in hex format (#FFCC00 is Michigan Tech yellow). GD via PHP tends to return an int. By doing some masking and converting to hex its easy to turn 16763904 -> 0xFF, 0xCC, 0x00, or #FFCC00.

Some future steps I may take is adding in some of those additional checks, factoring in the actual location of each pixel when deciding if its worth masking or not and trying to smooth out some of the curves. Also it'd be nice to completely drop the unnecessary pixels instead of just masking them, possibly saving the final image as a PNG or something else with transparency. For now, though, I'm quite happy to have replicated (in a somewhat rudimentary way) one of my old Photoshop tools with PHP.