43

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!

Anthony Mattox
  • 7,048
  • 6
  • 43
  • 59
  • 1
    I haven't read your code, but based on that graph, aren't you going to need to transform your RGB colour into HSV in order to work out where you are on that graph, so that you can work out how to move? – Oliver Charlesworth Dec 14 '11 at 16:24

15 Answers15

57

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

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • 3
    Just saved me several hours. How does this not have more upvotes?! – Escher Dec 10 '15 at 10:33
  • @Escher lots of reasons - 1. It took me days to fully flesh out the answer. 2. It's not something that lots of people need to do. 3. The obvious solution of converting to a color space with a hue component is simple and works well enough for many people. – Mark Ransom Dec 10 '15 at 13:14
  • I was live filtering a 320x180 video (which means 57600 pixels per frame) and RGB->HSV->RGB method was yielding 7 fps. When I started using this method, the fps actually dropped to 5. The thing is, unlike in RGB->HSV->RGB method you don't have to calculate everything for every pixel. When I made the matrix calculation per frame instead of per pixel, I ended up with 9 fps which made the video actually watchable. I can further improve my code to calculate the matrix only once instead of every frame to gain even more performance. This is what makes this method a lot better than RGB->HSV->RGB. – AlicanC Jan 19 '16 at 23:45
  • 1
    @AlicanC When I wrote the code I thought it would be obvious that `apply` would need to be called per pixel, and `set_hue_rotation` would only be used for setup. I guess I was wrong. – Mark Ransom Jan 19 '16 at 23:52
  • @MarkRansom it is obvious. It was just faster to implement it per pixel in the code I had so I did that first. – AlicanC Jan 21 '16 at 02:37
  • 1
    i find this solution to work best of all the one i found on SO. very good hue rotation – AndreaBogazzi Aug 06 '17 at 12:36
  • Nice. Note that your convention is using column vectors for colors, whereas the link for color transformations uses row vectors (i.e., matrices are transposed). – wcochran Nov 07 '17 at 14:02
  • This is a very effective an easy to implement solution! I re-wrote it for java and that works perfectly. Thank you! – JFreeman Apr 19 '19 at 00:07
  • Great solution, thanks! Why exactly do some results end up outside the [0,255] range? Is it due to numerical errors? – Attila Apr 24 '20 at 15:31
  • 2
    @Attila the diagonal of a 256x256x256 cube is longer than 256, so when you rotate it those corners just jut out. It's because RGB describes a cube and not a sphere. – Mark Ransom Apr 24 '20 at 16:44
18

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;
}
Jacob Eggers
  • 9,062
  • 2
  • 25
  • 43
  • 19
    As the original author of the page you linked to, I'd like to point out that RGB->HSV and HSV->RGB are not linear matrix transforms. What that code is actually doing is transforming RGB->YIQ (which is a linear equivalent to HSV) and rotating through the IQ plane. It also doesn't generate quite the results that people expect sometimes. However, trying to explain that as well as why HSV is kind of a ridiculous color concept to begin with won't fit in this comment box. :) – fluffy Feb 09 '12 at 07:31
  • I'm unable to reproduce the correct results with your method, however, Mark Ransom's answer worked great. Here is an example: Input ([R,G,B],H) = ([86,52,30], 210) and the output is [-31,15,2] for your method, and [36,43,88] with Mark's. I don't think round-off error could account for this drastic difference, something is wrong. – MasterHD May 27 '15 at 15:09
  • @MasterHD It looks like I forgot the offsets, but I'm still getting a difference `[28,75,62]`, so I'm not sure what's off now. – Jacob Eggers May 27 '15 at 22:44
  • @mcd wrote: "**there's a sign switched in the last line of calculations.** `+ (.114-.886*U-.203*W)*in.b;` **should be** `+ (.114+.886*U-.203*W)*in.b;`". (The OP doesn't have enough reputation to comment.) – Nisse Engström Jun 02 '15 at 17:18
  • @fluffy I know this was years ago, but is it possible you can update your code with more significant digits? I'm running your code in Haxe (not C++) and I'm getting some strange results. I know for sure I'm losing some precision, and I wonder if more significant digits would help. – ashes999 Feb 21 '16 at 14:48
  • @ashes999 it is almost certainly not an issue of significant digits and more one of misplaced expectations. The YIQ color space gamut isn't a circle so rotating it can cause points to fall outside of the original rectangle. Plus you should never accumulate multiple rotations on a point regardless of what the point represents (color, position, etc) – fluffy Feb 22 '16 at 16:31
  • I would point out that the coefficients are incorrect assuming you are doing typical work on a typical display. This means that the weights *should* be BT.709 primaries, which are the same primary weights sRGB uses. The values listed are for BT. 601, which are derived via a very arcane set of contexts that does not exist in the contemporary world. – troy_s Oct 02 '16 at 18:16
11

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 value

typedef 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.

Gautham Kantharaju
  • 1,735
  • 1
  • 21
  • 24
MasterHD
  • 2,264
  • 1
  • 32
  • 41
  • Hey - thanks -I found if you do this in a loop however, the colors eventually darken, so you need to add a little boost of brightness. Prob some rounding loss. e.g I did: float bright = 1.01; int r = (float)fg.r * bright; if (r > 255) { r = 255; } fg.r = r; int g = (float)fg.g * bright; if (g > 255) { g = 255; } fg.g = g; int b = (float)fg.b * bright; if (b > 255) { b = 255; } fg.b = b; – Goblinhack Nov 15 '20 at 16:42
5

Javascript implementation (based on Vladimir's PHP above)

const deg = Math.PI / 180;

function rotateRGBHue(r, g, b, hue) {
  const cosA = Math.cos(hue * deg);
  const sinA = Math.sin(hue * deg);
  const neo = [
    cosA + (1 - cosA) / 3,
    (1 - cosA) / 3 - Math.sqrt(1 / 3) * sinA,
    (1 - cosA) / 3 + Math.sqrt(1 / 3) * sinA,
  ];
  const result = [
    r * neo[0] + g * neo[1] + b * neo[2],
    r * neo[2] + g * neo[0] + b * neo[1],
    r * neo[1] + g * neo[2] + b * neo[0],
  ];
  return result.map(x => uint8(x));
}

function uint8(value) {
  return 0 > value ? 0 : (255 < value ? 255 : Math.round(value));
}
3

The post is old, and the original poster was looking for ios code - however, I was sent here via a search for visual basic code, so for all those like me, I converted Mark's code to a vb .net module:

Public Module HueAndTry    
    Public Function ClampIt(ByVal v As Double) As Integer    
        Return CInt(Math.Max(0F, Math.Min(v + 0.5, 255.0F)))    
    End Function    
    Public Function DegreesToRadians(ByVal degrees As Double) As Double    
        Return degrees * Math.PI / 180    
    End Function    
    Public Function RadiansToDegrees(ByVal radians As Double) As Double    
        Return radians * 180 / Math.PI    
    End Function    
    Public Sub HueConvert(ByRef rgb() As Integer, ByVal degrees As Double)
        Dim selfMatrix(,) As Double = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}
        Dim cosA As Double = Math.Cos(DegreesToRadians(degrees))
        Dim sinA As Double = Math.Sin(DegreesToRadians(degrees))
        Dim sqrtOneThirdTimesSin As Double = Math.Sqrt(1.0 / 3.0) * sinA
        Dim oneThirdTimesOneSubCos As Double = 1.0 / 3.0 * (1.0 - cosA)
        selfMatrix(0, 0) = cosA + (1.0 - cosA) / 3.0
        selfMatrix(0, 1) = oneThirdTimesOneSubCos - sqrtOneThirdTimesSin
        selfMatrix(0, 2) = oneThirdTimesOneSubCos + sqrtOneThirdTimesSin
        selfMatrix(1, 0) = selfMatrix(0, 2)
        selfMatrix(1, 1) = cosA + oneThirdTimesOneSubCos
        selfMatrix(1, 2) = selfMatrix(0, 1)
        selfMatrix(2, 0) = selfMatrix(0, 1)
        selfMatrix(2, 1) = selfMatrix(0, 2)
        selfMatrix(2, 2) = cosA + oneThirdTimesOneSubCos
        Dim rx As Double = rgb(0) * selfMatrix(0, 0) + rgb(1) * selfMatrix(0, 1) + rgb(2) * selfMatrix(0, 2)
        Dim gx As Double = rgb(0) * selfMatrix(1, 0) + rgb(1) * selfMatrix(1, 1) + rgb(2) * selfMatrix(1, 2)
        Dim bx As Double = rgb(0) * selfMatrix(2, 0) + rgb(1) * selfMatrix(2, 1) + rgb(2) * selfMatrix(2, 2)
        rgb(0) = ClampIt(rx)
        rgb(1) = ClampIt(gx)
        rgb(2) = ClampIt(bx)
    End Sub
End Module

I put common terms into (long) variables, but otherwise it's a straightforward conversion - worked fine for my needs.

By the way, I tried to leave Mark an upvote for his excellent code, but I didn't have enough votes myself to allow it to be visible (Hint, Hint).

Dave P.
  • 31
  • 1
  • 4
3

Basically there are two options:

  1. Convert RGB -> HSV, change hue, convert HSV -> RGB
  2. Change the hue directly with a linear transformation

I'm not really sure about how to implement 2, but basically you'll have to create a transformation matrix and filter the image through this matrix. However, this will re-color the image instead of changing only the hue. If this is ok for you, then this could be an option but if not a conversion cannot be avoided.

Edit

A little research shows this, which confirms my thoughts. To summarize: The conversion from RGB to HSV should be preferred, if an exact result is desired. Modifying the original RGB image by a linear transform also leads to a result but this rather tints the image. The difference is explained as follows: The conversion from RGB to HSV is non-linear, whereas the transform is linear.

Sebastian
  • 8,046
  • 2
  • 34
  • 58
2

WebGL version:

vec3 hueShift(vec3 col, float shift){
    vec3 m = vec3(cos(shift), -sin(shift) * .57735, 0);
    m = vec3(m.xy, -m.y) + (1. - m.x) * .33333;
    return mat3(m, m.zxy, m.yzx) * col;
}
1

Scott....not exactly. The algo seems to work the same as in HSL/HSV, but faster. Also, if you simply multiply the 1st 3 elements of the array with the factor for grey, you add/decrease luma.

Example...Greyscale from Rec709 have those values [GrayRedFactor_Rec709: R$ 0.212671 GrayGreenFactor_Rec709: R$ 0.715160 GrayBlueFactor_Rec709: R$ 0.072169]

When you multply self.matrix[x][x] with the GreyFactor correspondent you decrease luma without touching saturation Ex:

def set_hue_rotation(self, degrees):
    cosA = cos(radians(degrees))
    sinA = sin(radians(degrees))
    self.matrix[0][0] = (cosA + (1.0 - cosA) / 3.0) * 0.212671
    self.matrix[0][1] = (1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA) * 0.715160
    self.matrix[0][2] = (1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA) * 0.072169
    self.matrix[1][0] = self.matrix[0][2] <---Not sure, if this is the right code, but i think you got the idea
    self.matrix[1][1] = self.matrix[0][0]
    self.matrix[1][2] = self.matrix[0][1]

And the opposite is also true.If you divide instead multiply, the luminosity is increased dramatically.

From what i´m testing this algorithms can be a wonderfull replacement for HSL, as long you don´t need saturation, of course.

Try doing this...rotate the hue to only 1 degree (Just to force the algo to work properly while keeping the same perception sensitivity of the image), and multiply by those factors.

1

For anyone who needs the above described (gamma-uncorrected) hue shift as a parameterized HLSL Pixel shader (I through it together for a WPF application and thought I might even just share it):

    sampler2D implicitInput : register(s0);
    float factor : register(c0);

    float4 main(float2 uv : TEXCOORD) : COLOR
    {
            float4 color = tex2D(implicitInput, uv);

            float h = 360 * factor;          //Hue
            float s = 1;                     //Saturation
            float v = 1;                     //Value
            float M_PI = 3.14159265359;

            float vsu = v * s*cos(h*M_PI / 180);
            float vsw = v * s*sin(h*M_PI / 180);

            float4 result;
            result.r = (.299*v + .701*vsu + .168*vsw)*color.r
                            + (.587*v - .587*vsu + .330*vsw)*color.g
                            + (.114*v - .114*vsu - .497*vsw)*color.b;
            result.g = (.299*v - .299*vsu - .328*vsw)*color.r
                            + (.587*v + .413*vsu + .035*vsw)*color.g
                            + (.114*v - .114*vsu + .292*vsw)*color.b;
            result.b = (.299*v - .300*vsu + 1.25*vsw)*color.r
                            + (.587*v - .588*vsu - 1.05*vsw)*color.g
                            + (.114*v + .886*vsu - .203*vsw)*color.b;;
            result.a = color.a;

            return result;
    }
Robin B
  • 1,066
  • 1
  • 14
  • 32
1

It seems converting to HSV makes the most sense. Sass provides some amazing color helpers. It's in ruby, but it might provide useful.

http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html

0

Excelent code, but, i wonder that it can be faster if you simply don´t use self.matrix[2][0], self.matrix[2][1], self.matrix[2][1]

Therefore, set_hue_rotation can be written simply as:

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] = self.matrix[0][2] <---Not sure, if this is the right code, but i think you got the idea
    self.matrix[1][1] = self.matrix[0][0]
    self.matrix[1][2] = self.matrix[0][1]
0

Also, Mark´s algo produces more accurated results.

For instance, if you rotate the hue to 180º using HSV colorspace, the image may result in a reddish tone color.

But on Mark´s algo, the image is properly rotate. Skins tones for example (Hue = 17, Sat = 170, L = 160 in PSP) turns properly to blue that have Hue around 144 in PSP, and all other colors of the image are properly rotate.

The algo makes sense since Hue is nothing more, nothing else then a Logarithm function of a arctan of Red, Green, Blue as defined by this formula:

Hue = arctan((logR-logG)/(logR-logG+2*LogB))
Rahil Wazir
  • 10,007
  • 11
  • 42
  • 64
0

PHP implementation:

class Hue
{
    public function convert(int $r, int $g, int $b, int $hue)
    {
        $cosA = cos($hue * pi() / 180);
        $sinA = sin($hue * pi() / 180);

        $neo = [
            $cosA + (1 - $cosA) / 3,
            (1 - $cosA) / 3 - sqrt(1 / 3) * $sinA,
            (1 - $cosA) / 3 + sqrt(1 / 3) * $sinA,
        ];

        $result = [
            $r * $neo[0] + $g * $neo[1] + $b * $neo[2],
            $r * $neo[2] + $g * $neo[0] + $b * $neo[1],
            $r * $neo[1] + $g * $neo[2] + $b * $neo[0],
        ];

        return array_map([$this, 'crop'], $result);
    }

    private function crop(float $value)
    {
        return 0 > $value ? 0 : (255 < $value ? 255 : (int)round($value));
    }
}
  • 2
    Although your code snippet might solve the issue, you should describe what’s the purpose of your code (how it solves the problem). Furthermore, you might want to check https://stackoverflow.com/help/how-to-answer – Ahmad F Mar 06 '18 at 12:56
0

The most compact version on glsl, I was able to do:

vec3 hs(vec3 c, float s){
    vec3 m=vec3(cos(s),s=sin(s)*.5774,-s);
    return c*mat3(m+=(1.-m.x)/3.,m.zxy,m.yzx);
}
0

slightly changing MasterHD's answer to add Value and Saturation again we end up with the following C/C++ code:

#include <math.h>
typedef unsigned char uint8_t; //if no posix defs, remove if not needed

//if you use C not C++ this needs to be typedef ..
struct Color{
    uint8_t r;
    uint8_t g;
    uint8_t b;
};


uint8_t 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 (uint8_t)v;
}

//compare http://beesbuzz.biz/code/16-hsv-color-transforms
Color change_hsv_c(
    const Color &in, 
    const float fHue,
    const float fSat,
    const float fVal
)
{
    Color out;
    const float cosA = fSat*cos(fHue*3.14159265f/180); //convert degrees to radians
    const float sinA = fSat*sin(fHue*3.14159265f/180); //convert degrees to radians

    //helpers for faster calc //first 2 could actually be precomputed
    const float aThird = 1.0f/3.0f;
    const float rootThird = sqrtf(aThird);
    const float oneMinusCosA = (1.0f - cosA);
    const float aThirdOfOneMinusCosA = aThird * oneMinusCosA;
    const float rootThirdTimesSinA =  rootThird * sinA;
    const float plus = aThirdOfOneMinusCosA +rootThirdTimesSinA;
    const float minus = aThirdOfOneMinusCosA -rootThirdTimesSinA;

    //calculate the rotation matrix
    float matrix[3][3] = {
        {   cosA + oneMinusCosA / 3.0f  , minus                         , plus                          },
        {   plus                        , cosA + aThirdOfOneMinusCosA   , minus                         },
        {   minus                       , plus                          , cosA + aThirdOfOneMinusCosA   }
    };
    //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])*fVal);
    out.g = clamp((in.r*matrix[1][0] + in.g*matrix[1][1] + in.b*matrix[1][2])*fVal);
    out.b = clamp((in.r*matrix[2][0] + in.g*matrix[2][1] + in.b*matrix[2][2])*fVal);
    return out;
}
HannesH
  • 932
  • 2
  • 12