Last Updated: July 27, 2016
· royletron

Determining Image Contrast


Imagine the following horror scenario. You are creating a CMS in Rails, and for a certain page type you want to be able to have a heading area that has a user uploaded image as the background, and some text over the top of this image. You have no idea whether the user is going to upload an image that is completely white, or an image that is completely black, or something in between. So what colour do you make the text...

If you happen to be clever you could determine the contrast of the image, and then display the overlaid text in a colour that suits the image behind it, by doing this. To start with, it is assumed that you have taken care of your image with something like Paperclip, which I heartily recommend. Whatever you choose, you are going to need ImageMagick on the server. You will also need to have a form that allows the user to add their image and text, as it is the updating of this resource that is going to trigger the magic. So let's say this resource is called Cover and within cover you have an image and an overlay attribute, you will also need to add a boolean dark attribute to this to. So your Cover model should look like this:

class Cover < ActiveRecord::Base
  attr_accessible :dark, :overlay, :image
  has_attached_file :image, :styles => {:blur => "1000x600" }, :convert_options => { :blur => "-blur 0x8" } #this line is specific to Paperclip

The hasattachedfile is Paperclip specific, so if you are using something else to take care of your image don't worry. We are going to need to do is get a callback in on update, or on save as I have used:

after_save :determine_image_contrast

Then the function will then need to work out the contrast, and set the value of dark to true or false dependant on the image. Thankfully ImageMagick is your friend, and although slight hacky the following will do it:

def determine_image_contrast
  output = %x[convert  #{Dir.pwd}/public#{image.url(:original, timestamp: false)}  -colorspace gray  -resize 1x1  txt:- 2>&1]
  terms = /\#([a-zA-Z]|[0-9]){3,6}/.match(output).to_s.split(/(...)(..)(..)/)
  self.update_column(:dark, (terms[2].hex.to_i() < 130))
  return true

Breaking this down you can see the first line is running a command line function convert which is the main meat of what ImageMagick does, converting images into something. The first parameter is the image location #{Dir.pwd}/public#{image.url(:original, timestamp: false)} which is the running directory's public dir and then the relevant image within that. Again the image.url part is Paperclip specific and it is important to remove Paperclip's timestamp as it messes up the command. The next parts -colorspace gray -resize 1x1 txt:- is telling ImageMagick to render down the whole image to a 1x1 square, grayscale that square, and to output the result as text, which if you saw it would give you something like:

# ImageMagick pixel enumeration: 1,1,255,gray
0,0: (145,145,145)  #919191  gray(145,145,145)

The next line terms = /#([a-zA-Z]|[0-9]){3,6}/.match(output).to_s.split(/(...)(..)(..)/) pulls out the #hex value that represents the shade of grey, and breaks this down to its component R,G,B values (which are all the same). It then updates the dark value based on whether the G value is greater or less than 130, which is a threshold you can easily tweak. The important part here is to use the self.update_column function, anything else will trigger a save, which will in turn trigger this function and you end up with an infinite loop! Finally the function returns true, purely because I have read that some other functions down the line might require this, but I have yet to find out why! Once you have the data going in as the image is uploaded, you should test this first, you can then start playing around with the client side stuff, firstly adding some CSS like so:

  color: black;
  text-shadow: -1px 0px 1px #FFF, 1px 0px 1px #FFF, 0px 1px 1px #FFF, 0px -1px 1px #FFF;
  color: white;
  text-shadow: -1px 0px 1px #000, 1px 0px 1px #000, 0px 1px 1px #000, 0px -1px 1px #000;

And then adding the following to your layouts that are using this particular resource:

.jumbotron{:style => "background-image:url(#{@cover.image.url(:blur)});"}
  %h1{:class => "#{@cover.dark ? 'jumbo-dark' : 'jumbo-light'}"} Hello nurse!

Which so far is doing a really impressive job for us. You could easily extend this by not grey scaling the image and actually analysing each of the RGB channels, so you could display blue on images with more red, yellow on green etc.