RGB to Philips Hue (HSB)

2019-02-06 07:11发布

问题:

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

回答1:

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


回答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.



回答3:

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.