Pedantic Semantics – Colorspace conversions

One of the most interesting running debates in my head is how to represent ‘color’ in computer graphics. First of all, some of the best resources available have written by Paul Bourke.  There is one page in paritcular which offers quite a bit related to color spaces and whatnot: Color Spaces.

There is one distinction to make.  There is a difference between a color and a pixel, even if they have similar namings.  A pixel is a representation of a color, that is appropriate for  a particular device.  In computer graphics, pixels are typically an RGB byte pair, like Red: (255, 0, 0).  That is, the pixel takes up 3 bytes of memory, each byte representing a value between 0 and 255.

A colorspace, is a bit different.  You might have the linear color space, also known as RGB, but it will represent the values in a range of 0..1.0, inclusive.  Thus, the same value of red might be represented as RGBColor Red = (1.0, 0, 0).

This is a source of confusion sometimes.  It gets particularly interesting when you want to convert from one form to another.  You could convert straight pixel values, or you could convert colorspace based values.  Really what you want in most cases is to deal with colorspace values, and then just transform to RGB Pixel values when you really need to.  But, storage concerns might prove otherwise, and most of the time we end up converting pixel values instead.

Ahhh, such are the injustices of life.

One of the things that I want to keep straight in my head though is how to do things properly if I actually want to maintain a separation between my colorspace and my pixel space representations.  So, I have a couple of colorspace structures to help me out.

First, I choose as a base colorspace the RGB color space.  Why this one and not one of the sRGB, or CIEXYZ, or what have you?  Mainly because it makes for some easy coding, and I’m only intending to use it for computer graphics and print, so I’m not going to bother myself with others.

There are two color spaces I care about.  RGB, the well known Red, Green, Blue.  Then there’s HSL (HSB), Hue Saturation, Lightness (Brightness).  RGB because it’s standard, and HSL because it’s very common, in environments such as processing.  The RGB color space will be the base.  Here’s how they are defined:


ffi.cdef[[
typedef struct {
double r,g,b;
} RGBColor, *PRGBColor;

typedef struct {
double r,g,b,a;
} RGBAColor, *PRGBAColor;

typedef struct {
double h, s, l;
} HSLColor, *PHSLColor;

]]

local RGBColor = ffi.typeof("RGBColor");
local RGBColor_mt = {
	__tostring = function(self)
		return string.format("%3.4f, %3.4f, %3.4f", self.r, self.g, self.b)
	end,

}
local RGBColor = ffi.metatype(RGBColor, RGBColor_mt)


local RGBAColor = ffi.typeof("RGBAColor");

Kind of heavy handed to use doubles for the values?  Well, these are colors, not pixels.  You’re probably not going to store them by the millions in a frame buffer.  More typically, you’re going to define a few hundred our thousand of them, and then create pixel values from them by the millions.  So, you want this base representation to have as much precision as possible.  Besides, double matches with the lua ‘number’ type, so there are no gratuitous conversions, as would be true if you stored the values as ‘float’.

Then there’s one convenience function to convert from a typical RGB pixel byte triplet to this RGBColor structure:


local RGBToRGBColor = function(r,g,b) return RGBColor(r/255, g/255, b/255) end

snow = RGBToColor(255,255,0);
bisque = RGBToColor(255,228,196);

print("snow: ", snow)
>> snow: 	1.0000, 1.0000, 0.0000
print("bisque", bisque)
>> bisque	1.0000, 0.8941, 0.7686

Next, I want to look at the HSL color space. There are a few ways I would like to construct HSL colors. Here are some examples:

local snow = RGBToColor(255,255,0);
local h1 = HSLColor();      -- default constructor
local h2 = HSLColor(snow)   -- construct from RGBColor value
local h3 = HSLColor(h2)     -- copy constructor, from HSLColor value
local h4 = HSLColor(46, 1.0, 0.59)  -- construct from HSL components
local r4 = h4:ToRGBColor(); -- convert to RGBColor representation

This is where metatypes really come in handy. If you were doing this in C++, you’d define a default constructor, and a couple of copy constructors, and a cast operator, and that would be that. In Lua, there’s not all that much operator overloading, so you end up putting these things into one place. So, here’s some of the HSL metatype:

HSLColor = ffi.typeof("HSLColor")
HSLColor_p = ffi.typeof("PHSLColor")

HSLColor_mt = {
	--[[
   		Calculate HSL from RGB
   		Hue is in degrees
   		Lightness is between 0 and 1
   		Saturation is between 0 and 1
	--]]
	__new = function(ct, ...)
		local nelems = select("#", ...)

		-- Default constructor
		if nelems == 0 then
			return ffi.new(ct)
		end

		-- element constructor
		if nelems == 3 then
			return ffi.new(ct, select(1,...), select(2,...), select(3,...));
		end

		-- Should have only 1 argument now
		if nelems ~= 1 then
			return nil
		end

		local c1 = select(1,...)

		-- Copy constructor
		if ffi.istype(HSLColor, c1) then
			return ffi.new(ct, HSLColor)
		end

		-- Must be Copy from RGBColor
		if not ffi.istype(RGBColor, c1) then
			return nil
		end

  		local c2 = ffi.new(ct);

   		local themin = MIN(c1.r,MIN(c1.g,c1.b));
   		local themax = MAX(c1.r,MAX(c1.g,c1.b));
   		local delta = themax - themin;
   		c2.l = (themin + themax) / 2;
   		c2.s = 0;
   			
   		if (c2.l > 0 and c2.l  0) then
      		if (themax == c1.r and themax ~= c1.g) then
       			c2.h = c2.h + (c1.g - c1.b) / delta;
         	end
      		if (themax == c1.g and themax ~= c1.b) then
       			c2.h = c2.h + (2 + (c1.b - c1.r) / delta);
         	end
      		if (themax == c1.b and themax ~= c1.r) then
         		c2.h = c2.h + (4 + (c1.r - c1.g) / delta);
         	end
      			c2.h = c2.h * 60;
   		end
   		return(c2);
	end,

	__tostring = function(self)
		return string.format("%3.2f, %3.2f, %3.2f", self.h, self.s, self.l)
	end,

	__index = {
		--[[
   			Calculate RGB from HSL, reverse of RGB2HSL()
   			Hue is in degrees
   			Lightness is between 0 and 1
   			Saturation is between 0 and 1
		--]]
		ToRGBColor = function(c1)
   			while (c1.h  360) do
      			c1.h = c1.h - 360;
   			end

   			local sat = RGBColor();
   			if (c1.h < 120) then
      			sat.r = (120 - c1.h) / 60.0;
      			sat.g = c1.h / 60.0;
      			sat.b = 0;
   			elseif (c1.h < 240) then
      			sat.r = 0;
      			sat.g = (240 - c1.h) / 60.0;
      			sat.b = (c1.h - 120) / 60.0;
   			else 
      			sat.r = (c1.h - 240) / 60.0;
      			sat.g = 0;
      			sat.b = (360 - c1.h) / 60.0;
   			end

   			sat.r = MIN(sat.r,1);
   			sat.g = MIN(sat.g,1);
   			sat.b = MIN(sat.b,1);

   			local ctmp = RGBColor();
   			ctmp.r = 2 * c1.s * sat.r + (1 - c1.s);
   			ctmp.g = 2 * c1.s * sat.g + (1 - c1.s);
   			ctmp.b = 2 * c1.s * sat.b + (1 - c1.s);

   			local c2 = RGBColor();
   			if (c1.l < 0.5) then
      			c2.r = c1.l * ctmp.r;
      			c2.g = c1.l * ctmp.g;
      			c2.b = c1.l * ctmp.b;
   			else 
      			c2.r = (1 - c1.l) * ctmp.r + 2 * c1.l - 1;
      			c2.g = (1 - c1.l) * ctmp.g + 2 * c1.l - 1;
      			c2.b = (1 - c1.l) * ctmp.b + 2 * c1.l - 1;
   			end

   			return c2;
		end,
	}
}
HSLColor = ffi.metatype(HSLColor, HSLColor_mt);	

The __new() operator deals with all the constructor business. That is, when you’re doing anything like: HSLColor(), the __new metamethod will be called. In this case, I set the parameters to ‘…’, that is the Lua way of saying “variable argument list”. The form: select(“#”,…) is how you says “how many arguments are there?”.

By checking the number of arguments, and the types of the arguments, you can get the various constructors. I find this to be interesting, because you have to resolve any ambiguities explicitly. If you were using the C++ compiler, it would throw up warnings when it saw ambiguities. I find that having to deal with them myself explicitly is probably a good thing as I’ll really have to think about the types that I’ll accept, and what I won’t, and what I will do if I see something I don’t like.

The “ToRGBColor()” function is a simple metamethod, which will be called on any HSL structure. It will create an instance of a RGBColor object. And that’s that.

I don’t get too fancy here, in that I don’t convert to RGB Pixel values. Instead, I leave that to the pixel class, so I don’t introduce a circular dependency.

And that’s about it. Just some simple color handling, as is different from pixel handling. I can add more color spaces, and more conversion routines, such as coming up with grayscale from a color value, or creating RGBColor from a frequency value. The key thing is to keep these all done in color spaces, rather than pixel spaces, then convert to pixel spaces when required. It’s a distinction that might not always make sense for pragmatic programming, but if you want to make such a distinction, you can.

Advertisements


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s