RGB to Philips Hue (HSB)
Asked Answered
P

5

19

I'm making a musicplayer in Processing for an assignment for school. The Philips Hue lights will make some corresponding visual effects. I wanted to make the visuals kinda unique for each song. So I fetched the cover art (using LastFM API) of the playing track to get the most frequent color and use this as a base for creating the other colors. The Philips Hue has a different way of showing colors namely (HSB). So I converted it via

Color.RGBtoHSB();

For ex. it gives me for R= 127, G=190, B=208 the values H= 0.5370371, S=0.38942307, B=0.8156863. Now I'm guessing they were calculated on base 1 so I multiplied the Brightness en Saturation by 255. And the Hue by 65535. (As seen on http://developers.meethue.com/1_lightsapi.html)

When setting these calculated values in the Philips Hue no matter what song is playing the color is always redish or white.

Did something go wrong with the conversion between RGB to HSB?

On popular request my code:

As a test:

Color c = Colorconverter.getMostCommonColour("urltoimage");
float[] f = Colorconverter.getRGBtoHSB(c);
ArrayList<Lamp> myLamps = PhilipsHue.getInstance().getMyLamps();
State state = new State();
state.setBri((int) Math.ceil(f[2]*255));
state.setSat((int) Math.ceil(f[1]*255));
state.setHue((int) Math.ceil(f[0]*65535));
state.setOn(true);
PhilipsHue.setState(myLamps.get(1), state);

The functions as seen above

    public static Color getMostCommonColour(String coverArtURL) {
            Color coulourHex = null;
            try {
                BufferedImage image = ImageIO.read(new URL(coverArtURL));
                int height = image.getHeight();
                int width = image.getWidth();

                Map m = new HashMap();
                for (int i = 0; i < width; i++) {
                    for (int j = 0; j < height; j++) {
                        int rgb = image.getRGB(i, j);
                        int[] rgbArr = getRGBArr(rgb);
                        // No grays ...
                        if (!isGray(rgbArr)) {
                            Integer counter = (Integer) m.get(rgb);
                            if (counter == null) {
                                counter = 0;
                            }
                            counter++;
                            m.put(rgb, counter);
                        }
                    }
                }

                coulourHex = getMostCommonColour(m);
                System.out.println(coulourHex);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return coulourHex;
        }

    private static Color getMostCommonColour(Map map) {
            List list = new LinkedList(map.entrySet());
            Collections.sort(list, new Comparator() {
                public int compare(Object o1, Object o2) {
                    return ((Comparable) ((Map.Entry) (o1)).getValue())
                            .compareTo(((Map.Entry) (o2)).getValue());
                }
            });
            Map.Entry me = (Map.Entry) list.get(list.size() - 1);
            int[] rgb = getRGBArr((Integer) me.getKey());
            String r = Integer.toHexString(rgb[0]);
            String g = Integer.toHexString(rgb[1]);
            String b = Integer.toHexString(rgb[2]);
            Color c = new Color(rgb[0], rgb[1], rgb[2]);
            return c;
        }
private static int[] getRGBArr(int pixel) {
        int alpha = (pixel >> 24) & 0xff;
        int red = (pixel >> 16) & 0xff;
        int green = (pixel >> 8) & 0xff;
        int blue = (pixel) & 0xff;
        return new int[] { red, green, blue };

    }

    private static boolean isGray(int[] rgbArr) {
        int rgDiff = rgbArr[0] - rgbArr[1];
        int rbDiff = rgbArr[0] - rgbArr[2];
        // Filter out black, white and grays...... (tolerance within 10 pixels)
        int tolerance = 10;
        if (rgDiff > tolerance || rgDiff < -tolerance)
            if (rbDiff > tolerance || rbDiff < -tolerance) {
                return false;
            }
        return true;
    }

    public static float[] getRGBtoHSB(Color c) {
        float[] hsv = new float[3];
        return Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), hsv);
    }

The set state just does a simple put to the philips light bulbs. When I check the JSON on the Affected Light bulb

{
    "state": {
        "on": true,
        "bri": 81,
        "hue": 34277,
        "sat": 18,
        "xy": [
            0.298,
            0.2471
        ],
        "ct": 153,
        "alert": "none",
        "effect": "none",
        "colormode": "hs",
        "reachable": true
    },
    "type": "Extended color light",
    "name": "Hue Spot 1",
    "modelid": "LCT003",
    "swversion": "66010732",
    "pointsymbol": {
        "1": "none",
        "2": "none",
        "3": "none",
        "4": "none",
        "5": "none",
        "6": "none",
        "7": "none",
        "8": "none"
    }
}
Persuader answered 21/3, 2014 at 16:25 Comment(12)
The HSB values look right. I went to colorpicker.com. The values it accepts for H, S, B have maximums of 360, 100, 100 (like Gary said), so your values translate to H=193, S=39, B=82, which shows up as a bluish color with RGB very close to your original values. I'd double-check the hardware documentation to find out exactly what values it's expecting (most importantly, what range of values).Greenroom
@GaryKlasen No, the Philips Hue API uses values of 0–255 for brightness and saturation, and 0–65535 for the hue angle.Trismus
Instead of testing the light with values computed from RGB, try hardcoding HSB values for known colors and make sure that the light is behaving properly. In other words, isolate the problem by determining whether your conversion is wrong, or the communication with the light is broken.Trismus
@Trismus Betcha that after all this discussion, the answer is going to be that somebody plugged some wire into the wrong hole. :)Greenroom
@Trismus The light bulbs do show correct colors when playing around with the official Philips App. There are three of the the chance that they are all broken at the same time, ...Persuader
Could your colorMode() be the culprit? processing.org/reference/colorMode_.html Being able to see some code is usually helpful.Reube
The Colors are sent to the Philips Hue Light Bulbs they are set on Colormode 'hs'. It doens't matter what colorMode Processing is using. developers.meethue.com/1_lightsapi.htmlPersuader
I see. Missed your last edit.Reube
I wasn't suggesting the bulbs were broken, but questioning whether the bug is in conversion or some later code. Simple tests to divide the search space is basic debugging strategy. Post an SSCCE, since the description of your code and its results don't match.Trismus
This dude seems to be doing the same thing as you. Like I said, no code means its all vague: everyhue.com/vanilla/discussion/111/processinghue-disco-lights/…Reube
@Trismus It does sent the right values to the light bulbs. You can check by surfing to ipadressBridgePhilipsHue//debug/clip.html. And do a get on /api/developname/lights/LightbulbnumberonwhichyoumadethechangesPersuader
Really off-topic, but couldn't help it: How many programmers does it take to program a light bulb ? :PGluconeogenesis
P
19

A special thanks to StackOverflow user, Gee858eeG, to notice my typo and Erickson for the great tips and links.

Here is a working function to convert any RGB color to a Philips Hue XY values. The list returned contains just two element 0 being X, 1 being Y. The code is based on this brilliant note: https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/commit/f41091cf671e13fe8c32fcced12604cd31cceaf3

Eventhought this doesn't return the HSB value the XY values can be used as a replacement for changing colors on the Hue.Hopefully it can be helpful for other people, because Philips' API doesn't mention any formula.

public static List<Double> getRGBtoXY(Color c) {
    // For the hue bulb the corners of the triangle are:
    // -Red: 0.675, 0.322
    // -Green: 0.4091, 0.518
    // -Blue: 0.167, 0.04
    double[] normalizedToOne = new double[3];
    float cred, cgreen, cblue;
    cred = c.getRed();
    cgreen = c.getGreen();
    cblue = c.getBlue();
    normalizedToOne[0] = (cred / 255);
    normalizedToOne[1] = (cgreen / 255);
    normalizedToOne[2] = (cblue / 255);
    float red, green, blue;

    // Make red more vivid
    if (normalizedToOne[0] > 0.04045) {
        red = (float) Math.pow(
                (normalizedToOne[0] + 0.055) / (1.0 + 0.055), 2.4);
    } else {
        red = (float) (normalizedToOne[0] / 12.92);
    }

    // Make green more vivid
    if (normalizedToOne[1] > 0.04045) {
        green = (float) Math.pow((normalizedToOne[1] + 0.055)
                / (1.0 + 0.055), 2.4);
    } else {
        green = (float) (normalizedToOne[1] / 12.92);
    }

    // Make blue more vivid
    if (normalizedToOne[2] > 0.04045) {
        blue = (float) Math.pow((normalizedToOne[2] + 0.055)
                / (1.0 + 0.055), 2.4);
    } else {
        blue = (float) (normalizedToOne[2] / 12.92);
    }

    float X = (float) (red * 0.649926 + green * 0.103455 + blue * 0.197109);
    float Y = (float) (red * 0.234327 + green * 0.743075 + blue * 0.022598);
    float Z = (float) (red * 0.0000000 + green * 0.053077 + blue * 1.035763);

    float x = X / (X + Y + Z);
    float y = Y / (X + Y + Z);

    double[] xy = new double[2];
    xy[0] = x;
    xy[1] = y;
    List<Double> xyAsList = Doubles.asList(xy);
    return xyAsList;
}
Persuader answered 26/3, 2014 at 1:28 Comment(5)
I converted this over to python for a project. gist.github.com/error454/6b94c46d1f7512ffe5eeNorther
List<Double> xyAsList = Doubles.asList(xy); , in this line its showing error for "Doubles". Also android.graphics.Color not contain methods for getRed, getGreenGloat
Doubles class comes from guava. You can use this instead (Java 8>) DoubleStream.of(xy).boxed().collect(Collectors.toList());:Sidonia
Really nice Post! I rebuilt this logic in Typescript for a colorpicker. Do you also get no real colorfull yellow with your logic?Usurer
There are now more details on the official documentation, in the page "Color Conversion Formulas RGB to XY and back": developers.meethue.com/develop/application-design-guidance/…Groove
T
2

I think the problem here is that the Hue has a pretty limited color gamut. It's heavy on reds and purples, but can't produce as much saturation in the blue-green region..

I would suggest setting the saturation to the maximum, 255, and vary only the hue.

Based on the table given in the documentation, the Hue's "hue" attribute doesn't map directly to HSV's hue. The approximation might be close enough, but if not, it might be worthwhile to try a conversion to CIE 1931 color space, and then set the "xy" attribute instead of the hue.

Trismus answered 21/3, 2014 at 22:31 Comment(1)
I tried converting RGB value to XY, but I seem to be unable to get it just right. Read my full explanationPersuader
P
2

This post was about the only good hit I got when googling this 8 years later. Here's the Python version of Philips' Meethue Developer doc conversion routine (gamma settings are a bit different I think):

def rgb2xyb(r,g,b):
    r = ((r+0.055)/1.055)**2.4 if r > 0.04045 else r/12.92
    g = ((g+0.055)/1.055)**2.4 if g > 0.04045 else g/12.92
    b = ((b+0.055)/1.055)**2.4 if b > 0.04045 else b/12.92

    X = r * 0.4124 + g * 0.3576 + b * 0.1805
    Y = r * 0.2126 + g * 0.7152 + b * 0.0722
    Z = r * 0.0193 + g * 0.1192 + b * 0.9505

    return X / (X + Y + Z), Y / (X + Y + Z), int(Y*254)

It also returns brightness information in the range 0..254 as used by Hue Bridge API.

Precinct answered 20/3, 2022 at 22:14 Comment(1)
The relevant document (needs registration) developers.meethue.com/develop/application-design-guidance/… and a more thorough tutorial on using Hue with Python codeandlife.com/2022/03/20/…Precinct
J
1

Your RGB as HSB should be 193 degrees, 39% and 82% respectively. So at the very least S and B seem correct. Looking at the Philips hue API documentation, you're doing the right thing by multiplying those numbers by 255.

To get the H value as degrees, multiply the calculated H value by 360. That's how you arrive at the 193 in your case. Once you have the degrees, you multiply by 182 to get the value that you should send to the Philips hue API (from Hack the Hue):

hue
The parameters 'hue' and 'sat' are used to set the colour
The 'hue' parameter has the range 0-65535 so represents approximately 
182*degrees (technically 182.04 but the difference is imperceptible)

This should give you different H values than you're obtaining with the * 65535 method.

Jeanne answered 21/3, 2014 at 16:56 Comment(2)
Different, but only by the roundoff error you are introducing. The hue value returned by RGB to HSB conversion is a float from 0–1. 0.5370371 * 65535 = 35195, which is close to the 35126 value you get by rounding off the hue angle (193.33) and the conversion factor (182.04).Trismus
Thanks! But the Philips Light bulb still shows a white hue, instead of the blue as seen on colorpicker.com. Philips API says The hue value is a wrapping value between 0 and 65535. Both 0 and 65535 are red, 25500 is green and 46920 is blue. My value should be closer to 46920. If I make your calculation. 0.5370371 * 360° = 193.333356 multiply by 182 = 35186.67079Persuader
E
0

For those struggling with this issue and looking for a solution in Javascript, I convereted the top answer's response w/reference to @error454's python adaptation and confirmed it works with HUE bulbs and the API :D

function EnhanceColor(normalized) {
    if (normalized > 0.04045) {
        return Math.pow( (normalized + 0.055) / (1.0 + 0.055), 2.4);
    }
    else { return normalized / 12.92; }
        
}

function RGBtoXY(r, g, b) {
    let rNorm = r / 255.0;
    let gNorm = g / 255.0;
    let bNorm = b / 255.0;

    let rFinal = EnhanceColor(rNorm);
    let gFinal = EnhanceColor(gNorm);
    let bFinal = EnhanceColor(bNorm);

    let X = rFinal * 0.649926 + gFinal * 0.103455 + bFinal * 0.197109;
    let Y = rFinal * 0.234327 + gFinal * 0.743075 + bFinal * 0.022598;
    let Z = rFinal * 0.000000 + gFinal * 0.053077 + bFinal * 1.035763;

    if ( X + Y + Z === 0) {
        return [0,0];
    } else {
        let xFinal = X / (X + Y + Z);
        let yFinal = Y / (X + Y + Z);
    
        return [xFinal, yFinal];
    }

};

https://gist.github.com/NinjaBunny9000/fa81c231a9c205b5193bb76c95aeb75f

Erskine answered 6/7, 2021 at 2:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.