Programmer Art: HSV and You

Published November 24, 2008
Advertisement
As programmers, we are accustomed to dealing with images in RGB format. It's the 'native tongue' of the hardware, after all; RGB is the format the graphics card uses to move pixels around. The actual pixels themselves are comprised of clusters of red, green and blue units, so the translation from mathematical representation to on-screen representation is direct and straightforward. RGB vectors are simple to perform calculations and interpolations on, for lighting and shading and all the myriad operations one must perform in graphics programming. And the data representation of an RGB (or, of course, RGBA) pixel is simple and compact.

As Programmer Artists, though, we sometimes need to borrow some tricks from the 'real' artists in order to achieve a desired result. From the standpoint of an artist, RGB isn't the most intuitive or easiest way to work with color, texture, or image. Especially in the realms of programmatically constructing artwork, where it may be difficult to mimic the appearance of real-world surfaces by operating solely with RGB color scales and methods, we'll want to use a different representation.

Take the realm of procedural texture generation. You can approach procedural textures via a random approach, haphazardly slapping together fractal functions, color scales, and so forth, hoping for a result that makes sense, or you can approach it with a more logical method, having a goal in mind and analyzing the steps required to reach that goal, as any programmer would. You begin with a surface type you wish to synthesize, and you work backward, analyzing the components of that surface type until you understand the structures that comprise it. Yet, performing this sort of rigorous analysis on an RGB image can be difficult.

Enter the HSV representation of a color. At this point, the 'true' artists out there might be going, "well, duh". But many programmers are not accustomed to working with the HSV representation, and so it might not seem, to them, to be the logical approach.

To put it simply, HSV stands for "Hue, Saturation, Value", and represents a different way of looking at color. Rather than seeing a color as being comprised of "X amount of red, Y amount of green, Z amount of blue", the HSV system looks at the color being comprised of "X hue, or shade of color, Y amount of white, Z amount of brightness". The Hue aspect describes the overall 'pure' shade of color (red, green, blue, etc... all the many colors of the rainbow). The Saturation value describes how pure the color is. Combining white with the hue sort of washes it out, pushing the color toward white as saturation approaches 0, so that a non-saturated color is fully grey. And the Value quantity describes the brightness of the color, from black at value of 0 to the fully bright saturated hue at value of 1.

Why is this system useful you might ask? Because it cleanly separates the color model into intuitive concepts that the human mind is accustomed to working with. We understand the pure colors, we understand the 'greying out' of mixing white with a color, and we understand the bright/dark of well lit and shadowed areas instinctively. Many artists work exclusively in the HSV system, only converting to RGB when it is time to deliver the assets to the programmers who have to draw them on screen.

More to the point for our purposes, though, is that HSV allows us to separate 'color' from 'shading'. When working with a photograph of a real-world surface, and attempting to write a procedure to synthesize the texture, we need to be able to separately synthesize the colors that comprise a texture and the shading that provides detail. Analyzing a texture in this fashion using the typical RGB representation is difficult; we have no data set that says 'this is the color' and 'this is the shading detail'. We only have red, green, and blue channels, and separately analyzing them gives us no meaningful data from which to build a synthesis routine.

For the purpose of this discussion, we will store RGB colors as floating-point values in the range [0,1] for each component. The HSV values are represented a bit differently. In an HSV tuple, hue is a value in the range [0..360], which corresponds to a value on a 'color wheel'. The wheel is circular, transitioning from red, through orange and yellow, green, blue, violet, and back to red. The saturation and value quantities are stored as floating point values in the range [0,1]. For saturation, a value of 0 means the color is completely de-saturated (grey) and a value of 1 means it is fully saturated (pure). For value, a value of 0 means the color is fully dark (black) and a value of 1 means the color is fully bright. The three-coordinate system describes a cylinder. The top end of the cylinder consists of a color-wheel, with 'pure' hues at the outer rim, graduating to pure, bright white at the very center of the top disk. The central axis of the cylinder graduates from pure, bright white at the top (the center of the color wheel) to pure black at the bottom. The body of the cylinder contains all the possible hue, saturation and value combinations, describing a cone of color nestled within the cylinder.

The color palette for a single hue can be envisioned as a triangle. One point of the triangle is the pure, bright color. A second point of the triangle is pure white. The final point is pure black. The colors are smoothly interpolated in a 3-quantity gradient across the face of the triangle, representing all possible shades and saturations of that particular pure hue.

To begin playing around, we first need to be able to convert an RGB pixel to an HSV pixel, and back again. The conversions are fairly simple:

function min3(v1,v2,v3)  local cm=v1  if v2  if v3  return cmendfunction max3(v1,v2,v3)  local cm=v1  if v2>cm then cm=v2 end  if v3>cm then cm=v3 end  return cmendfunction RGB_to_HSV(rgb)  local mn,mx,dlt  local h,s,v    mn=min3(rgb.x, rgb.y, rgb.z)  mx=max3(rgb.x, rgb.y, rgb.z)  dlt=mx-mn  v=mx    if mx~=0 then    s=dlt/mx  else    -- s=0, h and v are undefined    return CRGBf(0,0,0)  end    if (rgb.x==mx) then    h=(rgb.y-rgb.z)/dlt  elseif (rgb.y==mx) then    h=2+(rgb.z-rgb.x)/dlt  else    h=4+(rgb.x-rgb.y)/dlt  end    h=h*60  if(h<0) then h = h+360 end    return CRGBf(h,s,v)endfunction HSV_to_RGB(hsv)  local i, f, p, q, t  local r,g,b  local h,s,v = hsv.x, hsv.y, hsv.z    if s==0 then    -- Grey    return CRGBf(v,v,v)  end    h=h/60  i=math.floor(h)  f=h-i  p=v*(1-s)  q=v*(1-s*f)  t=v*(1-s*(1-f))    if i==0 then    r=v g=t b=p  elseif i==1 then    r=q g=v b=p  elseif i==2 then    r=p g=v b=t  elseif i==3 then    r=p g=q b=t  elseif i==4 then    r=t g=p b=v  else    r=v g=p b=q  end    return CRGBf(r,g,b)endfunction extract_hsv(img, hue, saturation, value)  -- Extract HSV components from an rgb image  -- Buffer types: img=RGBf hue=RGBf saturation=double value=double  local w,h=img:getWidth(), img:getHeight()    local x,y  for x=0,w-1,1 do    for y=0,h-1,1 do      local pixel=img:get(x,y)      local hsv=RGB_to_HSV(pixel)      saturation:set(x,y,hsv.y)      value:set(x,y,hsv.z)      -- Set saturation to 1, value to 1 to extract 'pure' hue      hsv.y=1      hsv.z=1      hue:set(x,y,HSV_to_RGB(hsv))    end  endendfunction combine_hsv(img, hue, saturation, value)  -- Recombine HSV into RGB image  local w,h=img:getWidth(), img:getHeight()    local x,y  for x=0,w-1,1 do    for y=0,h-1,1 do      local hu=hue:get(x,y)      local hsv=RGB_to_HSV(hu)      hsv.y=saturation:get(x,y)      hsv.z=value:get(x,y)      img:set(x,y,HSV_to_RGB(hsv))    end  endend


Using just these functions we can easily break an image down into it's component H, S and V parts for further analysis. If you experiment with it, you will find that seemingly complex images are not always as truly complex as they seem to be, although unravelling that complexity requires a different analytical viewpoint than that provided by RGB representations. Take the following image:



This image was culled from a photograph I took of a rock surface while hiking in Flagstaff. It shows a lot of detail: minute variations in color, shading, detail, etc... Seems like it might be fairly difficult to synthesize a procedural texture that can mimic this surface with all its complexity, right?

Well, let's break it down into H, S and V maps, and see it from the viewpoint of a texture artist. Following is the breakdown. The Hue map shows the 'pure' colors that comprise the image, the Saturation and Value images are greyscale maps and show, of course, the saturation and brightness. Places where the Saturation map are whiter indicate places where the color is more 'pure'; places where it is darker indicate areas that are more washed-out and grey.

Hue:


Saturation:


Value:


As you can see, the surface really isn't as complex as it might seem. The colors used are more simple than one might think by looking at the original image, and the saturation and value maps look like something we could most likely approximate using our extensive library of fractal and noise functionality, right?

You may also be surprised at how bright and vivid the basic hues are, as well. This is to be expected, of course, as the hue component shows the pure colors of the spectrum upon which the image is founded. More elaborately colored surfaces may, of course, display a broader range of hues. This image is very simply colored, and yet when combined with the saturation and value layers for detail, the result is complex. By constructing a 3-stage fractal system (1 fractal system stage for each component) we could probably synthesize a fairly close representation of this surface for use in other applications. And the beauty of a purely procedural system is we can obtain near-infinite random variation without the extra cost of data storage. More on that later.
Previous Entry Still floored
Next Entry Plumbing
0 likes 2 comments

Comments

Jotaf
That's pretty cool :) The value field is usually associated with the shading (bright vs dark), so you can have one type of texture with the hue and saturation fields and slap a different sort of 3D structure on top of it (like grainy, smooth, or rocky like the one you show). It's pretty apparent from your example. With current shaders however it's probably better if you leave the value field relatively smooth and apply such structuring to the normal map or something; the shader will do the rest according to lighting conditions.
November 25, 2008 04:25 PM
Giallanon
very interesting
November 26, 2008 04:58 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement