Shift hue of an RGB Color Shift hue of an RGB Color c c

Shift hue of an RGB Color


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,radiansdef 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


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;}


I was disappointed by most answers I found here, some were flawed and basically flat-out wrong. I ended up spending 3+ hours trying to figure this out. The answer by Mark Ransom is correct, but I want to offer a complete C solution that's also verified with MATLAB. I have tested this thoroughly, and here is the C code:

#include <math.h>typedef unsigned char BYTE; //define an "integer" that only stores 0-255 valuetypedef struct _CRGB //Define a struct to store the 3 color values{    BYTE r;    BYTE g;    BYTE b;}CRGB;BYTE clamp(float v) //define a function to bound and round the input float value to 0-255{    if (v < 0)        return 0;    if (v > 255)        return 255;    return (BYTE)v;}CRGB TransformH(const CRGB &in, const float fHue){    CRGB out;    const float cosA = cos(fHue*3.14159265f/180); //convert degrees to radians    const float sinA = sin(fHue*3.14159265f/180); //convert degrees to radians    //calculate the rotation matrix, only depends on Hue    float matrix[3][3] = {{cosA + (1.0f - cosA) / 3.0f, 1.0f/3.0f * (1.0f - cosA) - sqrtf(1.0f/3.0f) * sinA, 1.0f/3.0f * (1.0f - cosA) + sqrtf(1.0f/3.0f) * sinA},        {1.0f/3.0f * (1.0f - cosA) + sqrtf(1.0f/3.0f) * sinA, cosA + 1.0f/3.0f*(1.0f - cosA), 1.0f/3.0f * (1.0f - cosA) - sqrtf(1.0f/3.0f) * sinA},        {1.0f/3.0f * (1.0f - cosA) - sqrtf(1.0f/3.0f) * sinA, 1.0f/3.0f * (1.0f - cosA) + sqrtf(1.0f/3.0f) * sinA, cosA + 1.0f/3.0f * (1.0f - cosA)}};    //Use the rotation matrix to convert the RGB directly    out.r = clamp(in.r*matrix[0][0] + in.g*matrix[0][1] + in.b*matrix[0][2]);    out.g = clamp(in.r*matrix[1][0] + in.g*matrix[1][1] + in.b*matrix[1][2]);    out.b = clamp(in.r*matrix[2][0] + in.g*matrix[2][1] + in.b*matrix[2][2]);    return out;}

NOTE: The rotation matrix only depends on the Hue (fHue), so once you've computed matrix[3][3], you can reuse it for every pixel in the image that is undergoing the same hue transformation! This will improve the efficiency drastically.Here is a MATLAB code that verifies the results:

function out = TransformH(r,g,b,H)    cosA = cos(H * pi/180);    sinA = sin(H * pi/180);    matrix = [cosA + (1-cosA)/3, 1/3 * (1 - cosA) - sqrt(1/3) * sinA, 1/3 * (1 - cosA) + sqrt(1/3) * sinA;          1/3 * (1 - cosA) + sqrt(1/3) * sinA, cosA + 1/3*(1 - cosA), 1/3 * (1 - cosA) - sqrt(1/3) * sinA;          1/3 * (1 - cosA) - sqrt(1/3) * sinA, 1/3 * (1 - cosA) + sqrt(1/3) * sinA, cosA + 1/3 * (1 - cosA)];    in = [r, g, b]';    out = round(matrix*in);end

Here is a sample input/output that was reproduceable by both codes:

TransformH(86,52,30,210)ans =    36    43    88

So the input RGB of [86,52,30] was converted to [36,43,88] using a hue of 210.