LJITColors, there’s a name for that

I have written about curating color data in the past: Curating Data – Resene and Hollasch Colors

Now, here it is, two years later, and I find a reason to revisit the topic.  Really I just wanted to create a new repository on GitHub which isolated my color related stuff because I was finding it hard to find it.  So, I wrote some code, and put it in this LJITColors repository.

What’s there?  Well, first of all, you must have some color databases.  The allcolors.lua file contains color tables for; Crayola, Hollasch, Resene, SGI.  These are just the tables that I’ve curated from around the web that I find to be interesting.  A color table is nothing more than a name/value pair that might look like this:

 

local CrayolaColors = {
	navyblue = {25, 116, 210},
	seagreen = {159, 226, 191},
	screamingreen = {118, 255, 122},
	greenblue = {17, 100, 180},
	royalpurple = {120, 81, 169},
	bluegray = {102, 153, 204},
	blizzardblue = {172, 229, 238},

And at the end of the allcolors.lua file we find:

return {
	resene = ReseneColors,
	crayola = CrayolaColors,
	hollasch = HollaschColors,
	sgi = SGIColors,
}

This in and of itself is interesting because right away you can already do something as simple as:

local colordb = require("allcolors")
local screaminggreen = colordb.crayola.screaminggreen

And you’ll get the RGB triplet that represents the ‘screaminggreen’ color. OK, that might be useful for some use cases. But, since you have the data in a form that is easily searchable and maliable, you can do even more.

Let’s say you want to lookup a color based on a fragment of the name. You want some form of green, but you’re not sure what exactly, so you just want to explore. Let’s say you want to know all the colors in the Crayola set that have the word “yellow” in them:

 	local found = colorman.matchColorByName("yellow", colorman.colordb["crayola"], "crayola")

This returns a set of values that looks like:

    {dbname ="Crayola", name="lemmonyellow", color={255,244,79}}

   crayola ultrayellow          255  164  116
   crayola yellowgreen          197  227  132
   crayola lemonyellow          255  244   79
   crayola orangeyellow         248  213  104
   crayola yellow               252  232  131
   crayola yelloworange         255  174   66
   crayola greenyellow          240  232  145
   crayola unmellowyellow       255  255  102

Well, that’s pretty spiffy I think. And if you want to search the whole database, and not just one set, you can use this form:

	local found = getColorLikeName(pattern)

The ‘pattern’ is used in a lua string.find() function, so it can actually be a complex expression if you like.

Although looking up color values by name is useful and interesting, looking up by component values might be even more interesting. This is where things get really interesting though. Let’s imagine we want to lookup colors that appear to be ‘white’ {255, 255, 255}.

My first naïve attempt at doing this was to make the observation that the triplet is just a vector. Well, if I can find the angle between two vectors, then a ‘match’ is simply when two vectors have an angle close to zero. Yah!! That’s the ticket.

local function normalize(A)
	local mag = math.sqrt(A[1]*A[1] + A[2]*A[2] + A[3]*A[3])
	return {A[1]/mag, A[2]/mag, A[3]/mag}
end

-- linear algebra dot product
local function dot(A,B)
	return A[1]*B[1]+A[2]*B[2]+A[3]*B[3];
end

local function angleBetweenColors(A, B)
	local a = normalize(A)
	local b = normalize(B)
	local angle = acos(dot(a,b))

	return angle
end

local function matchColorByValue(color, db, dbname)
	local colors = {}

	for name, candidate in pairs(db) do
		local angle = angleBetweenColors(color, candidate)
		if (angle < 0.05) then
			table.insert(colors, {dbname=dbname, name=name, color = candidate})
		end
	end

	return colors;
end

Then I can just do the following to find the “white” colors:

	local found = colorman.matchColorByValue({255,255,255}, colorman.colordb.sgi, "sgi")

This results in about 250 entries that look like this:

       sgi seashell1            255  245  238
       sgi gainsboro            220  220  220
       sgi grey20                51   51   51
       sgi oldlace              253  245  230
       sgi grey85               217  217  217
       sgi grey30                77   77   77
       sgi gray73               186  186  186
       sgi grey45               115  115  115
       sgi gray31                79   79   79
       sgi grey51               130  130  130
       sgi gray92               235  235  235
       sgi grey                 190  190  190
       sgi grey38                97   97   97
       sgi grey62               158  158  158
       sgi gray27                69   69   69
       sgi gray39                99   99   99
       sgi grey11                28   28   28
       .
       .
       .

Huh? What’s that about? Well, upon further inspection, it did exactly what I asked. It found all the colors that have the same normalized vector. In that context, there’s no difference between {28, 28, 28} and {255, 255, 255}. Hmmm, so what I need to also take account of is the luminance value, so I get a similar direction and magnitude if you will.

--
-- Convert to luminance using ITU-R Recommendation BT.709 (CCIR Rec. 709)
-- This is the one that matches modern HDTV and LCD monitors
local function lumaBT709(c)
	local gray = 0.2125 * c[1] + 0.7154 * c[2] + 0.0721 * c[3]

	return gray;
end

local function matchColorByValue(color, db, dbname)
	local colors = {}
	local colorluma = lumaBT709(color)


	for name, candidate in pairs(db) do
		local angle = angleBetweenColors(color, candidate)
		local candidateluma = lumaBT709(candidate)
		if (angle < 0.05) and (abs(candidateluma - colorluma) < 5) then
			table.insert(colors, {dbname=dbname, name=name, color = candidate})
		end
	end

	return colors;
end

This time, the set is more like what I was after:

       sgi gray100              255  255  255
       sgi snow1                255  250  250
       sgi azure                240  255  255
       sgi ivory1               255  255  240
       sgi grey99               252  252  252
       sgi gray99               252  252  252
       sgi ivory                255  255  240
       sgi grey100              255  255  255
       sgi mintcream            245  255  250
       sgi floralwhite          255  250  240
       sgi snow                 255  250  250
       sgi honeydew1            240  255  240
       sgi azure1               240  255  255
       sgi honeydew             240  255  240
       sgi white                255  255  255

And, again, if I want to do it over the entire database:

	local found = colorman.getColorByValue({255,255,255})
    resene ricecake             255  254  240
    resene blackwhite           255  254  246
    resene bianca               252  251  243
    resene quarterpearllusta    255  253  244
    resene soapstone            255  251  249
    resene orchidwhite          255  253  243
    resene halfpearllusta       255  252  234
    resene apricotwhite         255  254  236
    resene romance              255  254  253
    resene clearday             233  255  253
    resene whitenectar          252  255  231
    resene butterywhite         255  252  234
    resene promenade            252  255  231
    resene wanwhite             252  255  249
    resene sugarcane            249  255  246
    resene hintofyellow         250  253  228
    resene seafog               252  255  249
    resene rocksalt             255  255  255
    resene travertine           255  253  232
    resene ceramic              252  255  249
    resene alabaster            255  255  255
    resene chileanheath         255  253  230
    resene dew                  234  255  254
    resene bridalheath          255  250  244
    resene hintofgrey           252  255  249
    resene islandspice          255  252  238
    resene chinaivory           252  255  231
    resene orangewhite          254  252  237
   crayola white                255  255  255
  hollasch azure                240  255  255
  hollasch snow                 255  250  250
  hollasch ivory                255  255  240
  hollasch titanium_white       252  255  240
  hollasch mint_cream           245  255  250
  hollasch floral_white         255  250  240
  hollasch white                255  255  255
  hollasch honeydew             240  255  240
       sgi gray100              255  255  255
       sgi snow1                255  250  250
       sgi azure                240  255  255
       sgi ivory1               255  255  240
       sgi grey99               252  252  252
       sgi gray99               252  252  252
       sgi ivory                255  255  240
       sgi grey100              255  255  255
       sgi mintcream            245  255  250
       sgi floralwhite          255  250  240
       sgi snow                 255  250  250
       sgi honeydew1            240  255  240
       sgi azure1               240  255  255
       sgi honeydew             240  255  240
       sgi white                255  255  255

Now we’re cooking with gas!

So, originally, I was interested in curating a few sets of colors just so that I’d always have some color sets readily available. Now, I’ve turned those color sets into a quick and dirty database, and thrown in some data set specific search routines which makes them much more useful. For color matching, I can imagine picking up a few pixels from a bitmap image, and doing color matching to find ways to blend and extend.

It’s all fun, and realizations like taking into account the luminance value, makes the learning that much more interesting.

So, there you have it.


Curating Data – Resene and Hollasch Colors

I found some more color data that I wanted to play with. Resene paints has a whole bunch of color swatches. I found one file full of them, and the data looks like this:

Acadia                     27   20    4
Acapulco                  124  176  161
Acorn                     106   93   27
Aero Blue                 201  255  229
Affair                    113   70  147
Afghan Tan                134   86   10

When I did the SGI Colors, I used the following code, relying on the string.find() function:

parseline = function(line)
	local starting, ending, n1, n2, n3, name = line:find("%s*(%d*)%s*(%d*)%s*(%d*)%s*([%a%d%s]*)")
	return tonumber(n1), tonumber(n2), tonumber(n3), name
end

In this case, it would shift around a bit because the color name is first, rather than last on the line, but otherwise, relatively the same.

One other problem with that mechanism though is that it doesn’t deal with the space that can be found in the color’s name. So, now I have a chance to do it slightly differently. I do observe that the data is columnar. That is, all the names start at column 1, the red values start at 27, green at 32, and blue at 37. With this knowledge, I can simply use substrings:

ConvertReseneFile = function(filename)
  parseline = function(line)
    -- get name, strip whitespace
    -- make lowercase
    local name = line:sub(1,26)
    name = string.gsub(name, "%s",'')
    name = name:lower();

    -- get the numeric strings, convert to numbers
    local r = tonumber(line:sub(27,29))
    local g = tonumber(line:sub(32,34))
    local b = tonumber(line:sub(37,39))

    return name, r, g, b
  end

  io.write("local ReseneColors = {\n");
  for line in io.lines(filename) do
    local name, red,green,blue = parseline(line)
    io.write(string.format("%s = {%d, %d, %d},\n",name, red, green, blue))
  end
  io.write("}\n");
end

ConvertReseneFile("ReseneRGB.txt")

To get the name, I do the following:

    local name = line:sub(1,26)
    name = string.gsub(name, "%s",'')
    name = name:lower();

That will get 26 characters from the front of the line. Then, using ‘gsub()’, replace all the ‘space’ characters with nothing, effectively removing them. This has the effect of both trimming whitespace from the end, as well as removing intervening whitespace. Then I gratuitously convert to lowercase.

The numeric values are easy. Just get the subtext from where the number column starts, and 3 characters later. Turn it into a number explicitly, and you’re done.

Next up, Steve Hollasch colors. The raw data looks like this:

wheat             245   222   179   0.9608   0.8706   0.7020
white             255   255   255   1.0000   1.0000   1.0000
white_smoke       245   245   245   0.9608   0.9608   0.9608
zinc_white        253   248   255   0.9900   0.9700   1.0000

Greys
cold_grey         128   138   135   0.5000   0.5400   0.5300
dim_grey          105   105   105   0.4118   0.4118   0.4118

Quite similar to the Resene colors, in that it is columnar. A couple of differences though. The field names have the ‘_’ character, and the sections have a name, that I’d like to preserve as a comment. So, the conversion looks like this:

ConvertHollaschFile = function(filename)
  parseline = function(line)
    -- get name, strip whitespace
    -- make lowercase
    local name = line:sub(1,18)
    name = string.gsub(name, "%s",'')
    name = name:lower();

    -- get the numeric strings, convert to numbers
    local r = tonumber(line:sub(19,21))
    local g = tonumber(line:sub(25,27))
    local b = tonumber(line:sub(31,33))

    return name, r, g, b
  end

  io.write("local HollaschColors = {\n");
  for line in io.lines(filename) do
    local name, red,green,blue = parseline(line)
    --print(name, red, green,blue)
    if name ~= "" then
      if red and green and blue then
        io.write(string.format("%s = {%d, %d, %d},\n",name, red, green, blue))
      else
        io.write(string.format("\n-- %s\n", name);
      end
    end
  end
  io.write("}\n");
end

ConvertHollaschFile("HollaschColors.txt")

It looks almost identical to the Resene code. There’s just a little bit more in the post processing to deal with the section titles as a comment in the output.

In the end, you have tables, which can easily be converted to JSON form, or used directly as databases, or what have you, but basically you’ve curated the data and converted it into a form which is much easier to deal with programmatically.

I find this to be a strong feature of the Lua language. The string library is fairly small, but it hits just the right set of features to make it useful. There are other environments in which this small task might be equally easy to deal with, but I like this one because the whole runtime is only 300K of code. In other environments, the regular expression library alone might be that big, and the rest of the runtime might run into the multi-megabyte size. So, not bad for a few minutes of work.