Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Shift hue of an RGB Color

I'm trying to write a function to shift the hue of an RGB color. Specifically I'm using it in an iOS app, but the math is universal.

The graph below shows how the R, G, and B values change with respect to the hue.

Graph of RGB values across hues

Looking at that it seems like it should be a relatively simple to write a function to shift the hue without doing any nasty conversions to a different color format which would introduce more error (which could be an issue if continue applying small shifts to a color), and I suspect would be more computationally expensive.

Here is what I have so far which sort of works. It works perfectly if you're shifting from pure yellow or cyan or magenta but otherwise it gets a little squiffy in some places.

Color4f ShiftHue(Color4f c, float d) {     if (d==0) {         return c;     }     while (d<0) {         d+=1;     }      d *= 3;      float original[] = {c.red, c.green, c.blue};     float returned[] = {c.red, c.green, c.blue};      // big shifts     for (int i=0; i<3; i++) {         returned[i] = original[(i+((int) d))%3];     }     d -= (float) ((int) d);     original[0] = returned[0];     original[1] = returned[1];     original[2] = returned[2];      float lower = MIN(MIN(c.red, c.green), c.blue);     float upper = MAX(MAX(c.red, c.green), c.blue);      float spread = upper - lower;     float shift  = spread * d * 2;      // little shift     for (int i = 0; i < 3; ++i) {         // if middle value         if (original[(i+2)%3]==upper && original[(i+1)%3]==lower) {             returned[i] -= shift;             if (returned[i]<lower) {                 returned[(i+1)%3] += lower - returned[i];                 returned[i]=lower;             } else                 if (returned[i]>upper) {                     returned[(i+2)%3] -= returned[i] - upper;                     returned[i]=upper;                 }             break;         }     }      return Color4fMake(returned[0], returned[1], returned[2], c.alpha); } 

I know you can do this with UIColors and shift the hue with something like this:

CGFloat hue; CGFloat sat; CGFloat bri; [[UIColor colorWithRed:parent.color.red green:parent.color.green blue:parent.color.blue alpha:1] getHue:&hue saturation:&sat brightness:&bri alpha:nil]; hue -= .03; if (hue<0) {     hue+=1; } UIColor *tempColor = [UIColor colorWithHue:hue saturation:sat brightness:bri alpha:1]; const float* components= CGColorGetComponents(tempColor.CGColor); color = Color4fMake(components[0], components[1], components[2], 1); 

but I'm not crazy about that as It only works in iOS 5, and between allocating a number of color objects and converting from RGB to HSB and then back it seems pretty overkill.

I might end up using a lookup table or pre-calculate the colors in my application, but I'm really curious if there's a way to make my code work. Thanks!

like image 211
Anthony Mattox Avatar asked Dec 14 '11 16:12

Anthony Mattox


2 Answers

The RGB color space describes a cube. It is possible to rotate this cube around the diagonal axis from (0,0,0) to (255,255,255) to effect a change of hue. Note that some of the results will lie outside of the 0 to 255 range and will need to be clipped.

I finally got a chance to code this algorithm. It's in Python but it should be easy to translate to the language of your choice. The formula for 3D rotation came from http://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle

Edit: If you saw the code I posted previously, please ignore it. I was so anxious to find a formula for the rotation that I converted a matrix-based solution into a formula, not realizing that the matrix was the best form all along. I've still simplified the calculation of the matrix using the constant sqrt(1/3) for axis unit vector values, but this is much closer in spirit to the reference and simpler in the per-pixel calculation apply as well.

from math import sqrt,cos,sin,radians  def clamp(v):     if v < 0:         return 0     if v > 255:         return 255     return int(v + 0.5)  class RGBRotate(object):     def __init__(self):         self.matrix = [[1,0,0],[0,1,0],[0,0,1]]      def set_hue_rotation(self, degrees):         cosA = cos(radians(degrees))         sinA = sin(radians(degrees))         self.matrix[0][0] = cosA + (1.0 - cosA) / 3.0         self.matrix[0][1] = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA         self.matrix[0][2] = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA         self.matrix[1][0] = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA         self.matrix[1][1] = cosA + 1./3.*(1.0 - cosA)         self.matrix[1][2] = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA         self.matrix[2][0] = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA         self.matrix[2][1] = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA         self.matrix[2][2] = cosA + 1./3. * (1.0 - cosA)      def apply(self, r, g, b):         rx = r * self.matrix[0][0] + g * self.matrix[0][1] + b * self.matrix[0][2]         gx = r * self.matrix[1][0] + g * self.matrix[1][1] + b * self.matrix[1][2]         bx = r * self.matrix[2][0] + g * self.matrix[2][1] + b * self.matrix[2][2]         return clamp(rx), clamp(gx), clamp(bx) 

Here are some results from the above:

Hue rotation example

You can find a different implementation of the same idea at http://www.graficaobscura.com/matrix/index.html

like image 116
Mark Ransom Avatar answered Oct 11 '22 10:10

Mark Ransom


Edit per comment changed "are all" to "can be linearly approximated by".
Edit 2 adding offsets.


Essentially, the steps you want are

RBG->HSV->Update hue->RGB 

Since these can be approximated by linear matrix transforms (i.e. they are associative), you can perform it in a single step without any nasty conversion or loss of precision. You just multiple the transform matrices with each other, and use that to transform your colors.

There's a quick step by step here http://beesbuzz.biz/code/hsv_color_transforms.php

Here's the C++ code (With the saturation and value transforms removed):

Color TransformH(     const Color &in,  // color to transform     float H ) {   float U = cos(H*M_PI/180);   float W = sin(H*M_PI/180);    Color ret;   ret.r = (.299+.701*U+.168*W)*in.r     + (.587-.587*U+.330*W)*in.g     + (.114-.114*U-.497*W)*in.b;   ret.g = (.299-.299*U-.328*W)*in.r     + (.587+.413*U+.035*W)*in.g     + (.114-.114*U+.292*W)*in.b;   ret.b = (.299-.3*U+1.25*W)*in.r     + (.587-.588*U-1.05*W)*in.g     + (.114+.886*U-.203*W)*in.b;   return ret; } 
like image 21
Jacob Eggers Avatar answered Oct 11 '22 09:10

Jacob Eggers