private Bitmap DrawBitmap(string model) { var gamut = CIE1931Gamut.ForModel(model); int dimension = 500; Bitmap b = new Bitmap(dimension, dimension); for (int x = 0; x < dimension; x++) { for (int y = 0; y < dimension; y++) { CIE1931Point point = new CIE1931Point(x / (dimension * 1.0), y / (dimension * 1.0)); var rgb = HueColorConverter.XYToRgb(point, model); Color c; if (point.x + point.y > 1.0) { c = Color.Black; } else if (gamut.Contains(point)) { c = Color.FromArgb((int)(rgb.R * 255.999), (int)(rgb.G * 255.999), (int)(rgb.B * 255.999)); } else { c = Color.FromArgb((int)(rgb.R * 127.999), (int)(rgb.G * 127.999), (int)(rgb.B * 127.999)); } // CIE1931 charts are drawn with y-increasing being upwards, not downwards as in bitmaps. b.SetPixel(x, (dimension - 1) - y, c); } } return b; }
public void ColorConversionRoundtripAllPoints() { // Use a consistent seed for test reproducability Random r = new Random(0); for (int trial = 0; trial < 1000; trial++) { CIE1931Point originalXy; // Randomly generate a test color that is at a valid CIE1931 coordinate. do { originalXy = new CIE1931Point(r.NextDouble(), r.NextDouble()); }while (originalXy.x + originalXy.y >= 1.0); RGBColor rgb = HueColorConverter.XYToRgb(originalXy, "LCT001"); var xy = HueColorConverter.RgbToXY(rgb, "LCT001"); // We expect a point that is both inside the lamp's gamut and the "wide gamut" // used for XYZ->RGB and RGB->XYZ conversion. // Conversion from XY to RGB var expectedXy = CIE1931Gamut.ForModel("LCT001").NearestContainedPoint(originalXy); expectedXy = CIE1931Gamut.PhilipsWideGamut.NearestContainedPoint(expectedXy); // RGB to XY expectedXy = CIE1931Gamut.ForModel("LCT001").NearestContainedPoint(expectedXy); AssertAreEqual(expectedXy, xy, 0.0001); } }
private Bitmap DrawBitmap(string model) { var gamut = CIE1931Gamut.ForModel(model); int dimension = 500; Bitmap b = new Bitmap(dimension, dimension); for (int x = 0; x < dimension; x++) { for (int y = 0; y < dimension; y++) { CIE1931Point point = new CIE1931Point(x / (dimension * 1.0), y / (dimension * 1.0)); var rgb = HueColorConverter.XYToRgb(point, model); Color c; if (point.x + point.y > 1.0) { c = Color.Black; } else if (gamut.Contains(point)) { c = Color.FromArgb((int)(rgb.R * 255.999), (int)(rgb.G * 255.999), (int)(rgb.B * 255.999)); } else { c = Color.FromArgb((int)(rgb.R * 127.999), (int)(rgb.G * 127.999), (int)(rgb.B * 127.999)); } // CIE1931 charts are drawn with y-increasing being upwards, not downwards as in bitmaps. b.SetPixel(x, (dimension - 1) - y, c); } } return(b); }
/// <summary> /// Find the distance between two points. /// </summary> /// <param name="one"></param> /// <param name="two"></param> /// <returns>the distance between point one and two</returns> private static double GetDistanceBetweenTwoPoints(CIE1931Point one, CIE1931Point two) { double dx = one.x - two.x; // horizontal difference double dy = one.y - two.y; // vertical difference double dist = Math.Sqrt(dx * dx + dy * dy); return(dist); }
public void GamutContainsWorksCorrectly() { Random r = new Random(0); for (int trial = 0; trial < 1000; trial++) { var point = new CIE1931Point(r.NextDouble(), r.NextDouble()); var gamutB = CIE1931Gamut.ForModel("LCT001"); Assert.AreEqual(ReferenceColorConverter.CheckPointInLampsReach(point), gamutB.Contains(point)); } }
public static CIE1931Point RgbToXY(RGBColor color, CIE1931Gamut?gamut) { // Apply gamma correction. Convert non-linear RGB colour components // to linear color intensity levels. double r = InverseGamma(color.R); double g = InverseGamma(color.G); double b = InverseGamma(color.B); // Hue bulbs (depending on the type) can display colors outside the sRGB gamut supported // by most computer screens. // To make sure all colors are selectable by the user, Philips in its implementation // decided to interpret all RGB colors as if they were from a wide (non-sRGB) gamut. // The effect of this is to map colors in sRGB to a broader gamut of colors the hue lights // can produce. // // This also results in some deviation of color on screen vs color in real-life. // // The Philips implementation describes the matrix below with the comment // "Wide Gamut D65", but the values suggest this is infact not a standard // gamut but some custom gamut. // // The coordinates of this gamut have been identified as follows: // red: (0.700607, 0.299301) // green: (0.172416, 0.746797) // blue: (0.135503, 0.039879) // // (substitute r = 1, g = 1, b = 1 in sequence into array below and convert // from XYZ to xyY coordinates). // The plotted chart can be seen here: http://imgur.com/zelKnSk // // Also of interest, the white point is not D65 (0.31271, 0.32902), but a slightly // shifted version at (0.322727, 0.32902). This might be because true D65 is slightly // outside Gamut B (the position of D65 in the linked chart is slightly inaccurate). double X = r * 0.664511f + g * 0.154324f + b * 0.162028f; double Y = r * 0.283881f + g * 0.668433f + b * 0.047685f; double Z = r * 0.000088f + g * 0.072310f + b * 0.986039f; CIE1931Point xyPoint = new CIE1931Point(0.0, 0.0); if ((X + Y + Z) > 0.0) { // Convert from CIE XYZ to CIE xyY coordinates. xyPoint = new CIE1931Point(X / (X + Y + Z), Y / (X + Y + Z)); } if (gamut.HasValue) { // The point, adjusted it to the nearest point that is within the gamut of the lamp, if neccessary. return(gamut.Value.NearestContainedPoint(xyPoint)); } return(xyPoint); }
public void ColorsOutsideGamutAdjustedToInBeInGamut() { // This green is in the gamut of LST001, but not LCT001. CIE1931Point outsideGreen = new CIE1931Point(0.18, 0.72); CIE1931Point gamutAGreen = new CIE1931Point(0.2151, 0.7106); CIE1931Point gamutBGreen = new CIE1931Point(0.409, 0.518); CIE1931Point gamutCGreen = new CIE1931Point(0.17, 0.7); AssertAreEqual(gamutAGreen, CIE1931Gamut.ForModel("LST001").NearestContainedPoint(outsideGreen), 0.0001); AssertAreEqual(gamutBGreen, CIE1931Gamut.ForModel("LCT001").NearestContainedPoint(outsideGreen), 0.0001); AssertAreEqual(gamutCGreen, CIE1931Gamut.ForModel("LST002").NearestContainedPoint(outsideGreen), 0.0001); }
public static RGBColor XYToRgb(CIE1931Point point, CIE1931Gamut?gamut) { if (gamut.HasValue) { // If the color is outside the lamp's gamut, adjust to the nearest color // inside the lamp's gamut. point = gamut.Value.NearestContainedPoint(point); } // Also adjust it to be in the Philips "Wide Gamut" if not already. // The wide gamut used for XYZ->RGB conversion does not quite contain all colors // all of the hue bulbs support. point = CIE1931Gamut.PhilipsWideGamut.NearestContainedPoint(point); // Convert from xyY to XYZ coordinates. double Y = 1.0; // Luminance double X = (Y / point.y) * point.x; double Z = (Y / point.y) * point.z; // The Philips implementation comments this matrix with "sRGB D65 conversion" // However, this is not the XYZ -> RGB conversion matrix for sRGB. Instead // the matrix that is the inverse of that in RgbToXY() is used. // See comment in RgbToXY() for more info. double r = X * 1.656492 - Y * 0.354851 - Z * 0.255038; double g = -X * 0.707196 + Y * 1.655397 + Z * 0.036152; double b = X * 0.051713 - Y * 0.121364 + Z * 1.011530; // Downscale color components so that largest component has an intensity of 1.0, // as we can't display colors brighter than that. double maxComponent = Math.Max(Math.Max(r, g), b); if (maxComponent > 1.0) { r /= maxComponent; g /= maxComponent; b /= maxComponent; } // We now have the (linear) amounts of R, G and B corresponding to the specified XY coordinates. // Since displays are non-linear, we must apply a gamma correction to get the pixel value. // For example, a pixel red value of 1.0 (255) is more than twice as bright as 0.5 (127). // We need to correct for this non-linearity. r = Gamma(r); g = Gamma(g); b = Gamma(b); // Philips applies a second round of downscaling here, but that should be unnecessary given // gamma returns a value between 0.0 and 1.0 for every input between 0.0 and 1.0. return(new RGBColor(r, g, b)); }
public void ColorsOutsideGamutAdjustedToInBeInGamutOnConversion() { // The green primary of Gamut A. CIE1931Point gamutGreen = new CIE1931Point(0.2151, 0.7106); // A color green outside Gamut A. CIE1931Point greenOutsideGamut = new CIE1931Point(0.21, 0.75); var a = HueColorConverter.XYToRgb(gamutGreen, "LST001"); var b = HueColorConverter.XYToRgb(greenOutsideGamut, "LST001"); // Points should be equal, since the green outside the gamut should // be adjusted the the nearest green in-gamut. Assert.AreEqual(a.R, b.R); Assert.AreEqual(a.G, b.G); Assert.AreEqual(a.B, b.B); }
/// <summary> /// Method to see if the given XY value is within the reach of the lamps. /// </summary> /// <param name="p">p the point containing the X,Y value</param> /// <returns>true if within reach, false otherwise.</returns> public static bool CheckPointInLampsReach(CIE1931Point p) { CIE1931Point v1 = new CIE1931Point(Lime.x - Red.x, Lime.y - Red.y); CIE1931Point v2 = new CIE1931Point(Blue.x - Red.x, Blue.y - Red.y); CIE1931Point q = new CIE1931Point(p.x - Red.x, p.y - Red.y); double s = ReferenceColorConverter.CrossProduct(q, v2) / ReferenceColorConverter.CrossProduct(v1, v2); double t = ReferenceColorConverter.CrossProduct(v1, q) / ReferenceColorConverter.CrossProduct(v1, v2); if ((s >= 0.0f) && (t >= 0.0f) && (s + t <= 1.0f)) { return(true); } else { return(false); } }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.StartArray) { JArray array = JArray.Load(reader); var gammutValues = array.ToObject <IList <IList <double> > >(); if (gammutValues != null && gammutValues.Count == 3) { var red = new CIE1931Point(gammutValues[0][0], gammutValues[0][1]); var green = new CIE1931Point(gammutValues[1][0], gammutValues[1][1]); var blue = new CIE1931Point(gammutValues[2][0], gammutValues[2][1]); return(new CIE1931Gamut(red, green, blue)); } } return(null); }
/// <summary> /// Find the closest point on a line. /// This point will be within reach of the lamp. /// </summary> /// <param name="A">A the point where the line starts</param> /// <param name="B">B the point where the line ends</param> /// <param name="P">P the point which is close to a line.</param> /// <returns> the point which is on the line.</returns> private static CIE1931Point GetClosestPointToPoint(CIE1931Point A, CIE1931Point B, CIE1931Point P) { CIE1931Point AP = new CIE1931Point(P.x - A.x, P.y - A.y); CIE1931Point AB = new CIE1931Point(B.x - A.x, B.y - A.y); double ab2 = AB.x * AB.x + AB.y * AB.y; double ap_ab = AP.x * AB.x + AP.y * AB.y; double t = ap_ab / ab2; if (t < 0.0f) { t = 0.0f; } else if (t > 1.0f) { t = 1.0f; } CIE1931Point newPoint = new CIE1931Point(A.x + AB.x * t, A.y + AB.y * t); return(newPoint); }
public void ColorConversionRoundtripInsideGamut() { // Use a consistent seed for test reproducability Random r = new Random(0); for (int trial = 0; trial < 1000; trial++) { CIE1931Point originalXy; // Randomly generate a test color that is at a valid CIE1931 coordinate. do { originalXy = new CIE1931Point(r.NextDouble(), r.NextDouble()); }while (originalXy.x + originalXy.y >= 1.0 || !ReferenceColorConverter.CheckPointInLampsReach(originalXy) || !CIE1931Gamut.PhilipsWideGamut.Contains(originalXy)); RGBColor rgb = HueColorConverter.XYToRgb(originalXy, "LCT001"); var xy = HueColorConverter.RgbToXY(rgb, "LCT001"); AssertAreEqual(originalXy, xy, 0.0001); } }
/// <summary> /// /// </summary> /// <param name="light"></param> /// <param name="xy"></param> /// <param name="gamut">The gamut to use</param> /// <param name="timeSpan"></param> /// <param name="cancellationToken"></param> /// <returns></returns> public static void SetColor(this EntertainmentLight light, CancellationToken cancellationToken, CIE1931Point xy, CIE1931Gamut gamut, TimeSpan timeSpan = default) { var rgb = HueColorConverter.XYToRgb(xy, gamut); light.SetState(cancellationToken, rgb, null, timeSpan); }
public void ColorConversionRoundtripInsideGamut() { // Use a consistent seed for test reproducability Random r = new Random(0); for (int trial = 0; trial < 1000; trial++) { CIE1931Point originalXy; // Randomly generate a test color that is at a valid CIE1931 coordinate. do { originalXy = new CIE1931Point(r.NextDouble(), r.NextDouble()); } while (originalXy.x + originalXy.y >= 1.0 || !ReferenceColorConverter.CheckPointInLampsReach(originalXy) || !CIE1931Gamut.PhilipsWideGamut.Contains(originalXy)); RGBColor rgb = HueColorConverter.XYToRgb(originalXy, "LCT001"); var xy = HueColorConverter.RgbToXY(rgb, "LCT001"); AssertAreEqual(originalXy, xy, 0.0001); } }
private void AssertAreEqual(CIE1931Point expected, CIE1931Point actual, double delta) { Assert.AreEqual(expected.x, actual.x, delta); Assert.AreEqual(expected.y, actual.y, delta); Assert.AreEqual(expected.z, actual.z, delta); }
/// <summary> /// Get XY from red,green,blue ints /// </summary> /// <param name="red"></param> /// <param name="green"></param> /// <param name="blue"></param> /// <returns></returns> public static CIE1931Point XyFromColor(double red, double green, double blue) { double r = (red > 0.04045f) ? Math.Pow((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); double g = (green > 0.04045f) ? Math.Pow((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); double b = (blue > 0.04045f) ? Math.Pow((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f); //double X = r * 0.4360747f + g * 0.3850649f + b * 0.0930804f; //double Y = r * 0.2225045f + g * 0.7168786f + b * 0.0406169f; //double Z = r * 0.0139322f + g * 0.0971045f + b * 0.7141733f; double X = r * 0.664511f + g * 0.154324f + b * 0.162028f; double Y = r * 0.283881f + g * 0.668433f + b * 0.047685f; double Z = r * 0.000088f + g * 0.072310f + b * 0.986039f; double cx = X / (X + Y + Z); double cy = Y / (X + Y + Z); if (Double.IsNaN(cx)) { cx = 0.0f; } if (Double.IsNaN(cy)) { cy = 0.0f; } //Check if the given XY value is within the colourreach of our lamps. CIE1931Point xyPoint = new CIE1931Point(cx, cy); bool inReachOfLamps = ReferenceColorConverter.CheckPointInLampsReach(xyPoint); if (!inReachOfLamps) { //It seems the colour is out of reach //let's find the closes colour we can produce with our lamp and send this XY value out. //Find the closest point on each line in the triangle. CIE1931Point pAB = ReferenceColorConverter.GetClosestPointToPoint(Red, Lime, xyPoint); CIE1931Point pAC = ReferenceColorConverter.GetClosestPointToPoint(Blue, Red, xyPoint); CIE1931Point pBC = ReferenceColorConverter.GetClosestPointToPoint(Lime, Blue, xyPoint); //Get the distances per point and see which point is closer to our Point. double dAB = ReferenceColorConverter.GetDistanceBetweenTwoPoints(xyPoint, pAB); double dAC = ReferenceColorConverter.GetDistanceBetweenTwoPoints(xyPoint, pAC); double dBC = ReferenceColorConverter.GetDistanceBetweenTwoPoints(xyPoint, pBC); double lowest = dAB; CIE1931Point closestPoint = pAB; if (dAC < lowest) { lowest = dAC; closestPoint = pAC; } if (dBC < lowest) { lowest = dBC; closestPoint = pBC; } //Change the xy value to a value which is within the reach of the lamp. cx = closestPoint.x; cy = closestPoint.y; } return(new CIE1931Point(cx, cy)); }
/// <summary> /// Find the closest point on a line. /// This point will be within reach of the lamp. /// </summary> /// <param name="A">A the point where the line starts</param> /// <param name="B">B the point where the line ends</param> /// <param name="P">P the point which is close to a line.</param> /// <returns> the point which is on the line.</returns> private static CIE1931Point GetClosestPointToPoint(CIE1931Point A, CIE1931Point B, CIE1931Point P) { CIE1931Point AP = new CIE1931Point(P.x - A.x, P.y - A.y); CIE1931Point AB = new CIE1931Point(B.x - A.x, B.y - A.y); double ab2 = AB.x * AB.x + AB.y * AB.y; double ap_ab = AP.x * AB.x + AP.y * AB.y; double t = ap_ab / ab2; if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f; CIE1931Point newPoint = new CIE1931Point(A.x + AB.x * t, A.y + AB.y * t); return newPoint; }
/// <summary> /// Find the distance between two points. /// </summary> /// <param name="one"></param> /// <param name="two"></param> /// <returns>the distance between point one and two</returns> private static double GetDistanceBetweenTwoPoints(CIE1931Point one, CIE1931Point two) { double dx = one.x - two.x; // horizontal difference double dy = one.y - two.y; // vertical difference double dist = Math.Sqrt(dx * dx + dy * dy); return dist; }
/// <summary> /// Calculates crossProduct of two 2D vectors / points. /// </summary> /// <param name="p1"> p1 first point used as vector</param> /// <param name="p2">p2 second point used as vector</param> /// <returns>crossProduct of vectors</returns> private static double CrossProduct(CIE1931Point p1, CIE1931Point p2) { return (p1.x * p2.y - p1.y * p2.x); }
/// <summary> /// Calculates crossProduct of two 2D vectors / points. /// </summary> /// <param name="p1"> p1 first point used as vector</param> /// <param name="p2">p2 second point used as vector</param> /// <returns>crossProduct of vectors</returns> private static double CrossProduct(CIE1931Point p1, CIE1931Point p2) { return(p1.x * p2.y - p1.y * p2.x); }
/// <summary> /// Method to see if the given XY value is within the reach of the lamps. /// </summary> /// <param name="p">p the point containing the X,Y value</param> /// <returns>true if within reach, false otherwise.</returns> public static bool CheckPointInLampsReach(CIE1931Point p) { CIE1931Point v1 = new CIE1931Point(Lime.x - Red.x, Lime.y - Red.y); CIE1931Point v2 = new CIE1931Point(Blue.x - Red.x, Blue.y - Red.y); CIE1931Point q = new CIE1931Point(p.x - Red.x, p.y - Red.y); double s = ReferenceColorConverter.CrossProduct(q, v2) / ReferenceColorConverter.CrossProduct(v1, v2); double t = ReferenceColorConverter.CrossProduct(v1, q) / ReferenceColorConverter.CrossProduct(v1, v2); if ((s >= 0.0f) && (t >= 0.0f) && (s + t <= 1.0f)) { return true; } else { return false; } }
/// <summary> /// Get XY from red,green,blue ints /// </summary> /// <param name="red"></param> /// <param name="green"></param> /// <param name="blue"></param> /// <returns></returns> public static CIE1931Point XyFromColor(double red, double green, double blue) { double r = (red > 0.04045f) ? Math.Pow((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); double g = (green > 0.04045f) ? Math.Pow((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); double b = (blue > 0.04045f) ? Math.Pow((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f); //double X = r * 0.4360747f + g * 0.3850649f + b * 0.0930804f; //double Y = r * 0.2225045f + g * 0.7168786f + b * 0.0406169f; //double Z = r * 0.0139322f + g * 0.0971045f + b * 0.7141733f; double X = r * 0.664511f + g * 0.154324f + b * 0.162028f; double Y = r * 0.283881f + g * 0.668433f + b * 0.047685f; double Z = r * 0.000088f + g * 0.072310f + b * 0.986039f; double cx = X / (X + Y + Z); double cy = Y / (X + Y + Z); if (Double.IsNaN(cx)) { cx = 0.0f; } if (Double.IsNaN(cy)) { cy = 0.0f; } //Check if the given XY value is within the colourreach of our lamps. CIE1931Point xyPoint = new CIE1931Point(cx, cy); bool inReachOfLamps = ReferenceColorConverter.CheckPointInLampsReach(xyPoint); if (!inReachOfLamps) { //It seems the colour is out of reach //let's find the closes colour we can produce with our lamp and send this XY value out. //Find the closest point on each line in the triangle. CIE1931Point pAB = ReferenceColorConverter.GetClosestPointToPoint(Red, Lime, xyPoint); CIE1931Point pAC = ReferenceColorConverter.GetClosestPointToPoint(Blue, Red, xyPoint); CIE1931Point pBC = ReferenceColorConverter.GetClosestPointToPoint(Lime, Blue, xyPoint); //Get the distances per point and see which point is closer to our Point. double dAB = ReferenceColorConverter.GetDistanceBetweenTwoPoints(xyPoint, pAB); double dAC = ReferenceColorConverter.GetDistanceBetweenTwoPoints(xyPoint, pAC); double dBC = ReferenceColorConverter.GetDistanceBetweenTwoPoints(xyPoint, pBC); double lowest = dAB; CIE1931Point closestPoint = pAB; if (dAC < lowest) { lowest = dAC; closestPoint = pAC; } if (dBC < lowest) { lowest = dBC; closestPoint = pBC; } //Change the xy value to a value which is within the reach of the lamp. cx = closestPoint.x; cy = closestPoint.y; } return new CIE1931Point(cx, cy); }
public void ColorConversionRoundtripAllPoints() { // Use a consistent seed for test reproducability Random r = new Random(0); for (int trial = 0; trial < 1000; trial++) { CIE1931Point originalXy; // Randomly generate a test color that is at a valid CIE1931 coordinate. do { originalXy = new CIE1931Point(r.NextDouble(), r.NextDouble()); } while (originalXy.x + originalXy.y >= 1.0); RGBColor rgb = HueColorConverter.XYToRgb(originalXy, "LCT001"); var xy = HueColorConverter.RgbToXY(rgb, "LCT001"); // We expect a point that is both inside the lamp's gamut and the "wide gamut" // used for XYZ->RGB and RGB->XYZ conversion. // Conversion from XY to RGB var expectedXy = CIE1931Gamut.ForModel("LCT001").NearestContainedPoint(originalXy); expectedXy = CIE1931Gamut.PhilipsWideGamut.NearestContainedPoint(expectedXy); // RGB to XY expectedXy = CIE1931Gamut.ForModel("LCT001").NearestContainedPoint(expectedXy); AssertAreEqual(expectedXy, xy, 0.0001); } }
/// <summary> /// Set state on a single light /// </summary> /// <param name="light"></param> /// <param name="xy"></param> /// <param name="gamut"></param> /// <param name="brightness"></param> /// <param name="timeSpan"></param> /// <param name="cancellationToken"></param> /// <returns></returns> public static void SetState(this EntertainmentLight light, CancellationToken cancellationToken, CIE1931Point xy, CIE1931Gamut gamut, double?brightness = null, TimeSpan timeSpan = default(TimeSpan)) { var rgb = HueColorConverter.XYToRgb(xy, gamut); //Create a new transition for this light Transition transition = new Transition(rgb, brightness, timeSpan); light.Transition = transition; //Start the transition transition.Start(light.State.RGBColor, light.State.Brightness, cancellationToken); }