The first attempt
Working on a project where users can customise background colour for their profile, I needed a way to test if a background colour is light or dark in order to choose an appropriate text colour – black on light colours and white on dark ones. My search for a solution began with me calculating the greyscale of colour, but any algorithm I tried produced mixed results that didn’t satisfy my needs.
Then I changed my approach and started to consider RGB colours as points in 3D space where r, g, n are the axes. In this case, black is a point represented by the coordinate (0, 0, 0). A colour is darker the closer it is to this “black” point. So, calculating the brightness of the colour became a question of calculating distance in 3D space, which could be answered with the formula:
sqrt(dx^2+dy^2+dz^2)
or in our case simply,
sqrt(x^2+y^2+z^2)
because black is (0, 0, 0).
Still not right
This solution, however, was not perfect. One of the problems that arose was related to the fact that red, green and blue have different wavelengths and consequently they have three different contributions in the formation of the final colour. The colour red has the longest wavelength of the three colours, while green has a shorter wavelength than red. Not only this, but green is also the colour that produces a more soothing effect on the eyes. This means we have to decrease the contribution of the red colour and increase the contribution of the green colour. In other words, we needed to find a way to adjust colour weight.
The final solution
There is no unique way to choose colour weight, so we decided to use the same algorithm for converting RGB to YIQ colour space used in standard colour TV and video systems in North America. Weights for red, green and blue are 0.299, 0.587 and 0.114. Knowing how to choose the right weight for each colour component required knowledge of photometry, however, this is beyond the scope of this blog.
Combining the colour weights and the distance formula presented earlier, we came up with a final solution. Below is the solution we implemented in ruby:
def brightness(pixel) Math.sqrt( 0.299 * pixel.red**2 + 0.587 * pixel.green**2 + 0.114 * pixel.blue**2 ).to_i end
Because brightness can be considered as a grayscale, we can use the same formula for converting coloured images into black-and-white. We have to replace each r, g, b component in every pixel with pixel brightness. Below is a ruby implementation and what we got as a result.
require 'RMagick' def brightness(pixel) Math.sqrt( 0.299 * pixel.red**2 + 0.587 * pixel.green**2 + 0.114 * pixel.blue**2 ).to_i end image = Magick::Image.read('bosmal.jpg')[0] image.each_pixel do |pixel, c, r| pixel.red = pixel.green = pixel.blue = brightness(pixel) image.pixel_color(c, r, pixel) end image.write("gray.jpg")
For reading image from file and saving to file solution using ImageMagick image processing library (check it out here) and RMagick gem (check it out here)