/ApplicationDesignNotes/RGB to xy Color conversion.md

https://github.com/kmissoumi/PhilipsHueSDK-iOS-OSX · Markdown · 363 lines · 297 code · 66 blank · 0 comment · 0 complexity · 59016d95f10f3a2e637700f9ba94778d MD5 · raw file

  1. #Application Design note - Color conversion ##Color conversion formulas RGB to xy and back This document describes the conversion between RGB and xy. It is important to differentiate between the various light types, because they do not all support the same color gamut. For example, the hue bulbs are very good at showing nice whites, while the LivingColors are generally a bit better at colors, like green and cyan. Our implementations of these functions are contained in the PHUtility class in our GitHub repo for the Hue SDK (https://github.com/PhilipsHue/PhilipsHueSDKiOS). You can see source code of our Objective-C iOS implementation of these transformations in the last part of this document.
  2. The method signature for converting from xy values and brightness to a color is: + (UIColor *)colorFromXY:(CGPoint)xy forModel:(NSString*)model The method signature for converting from a color to xy and brightness values:
  3. + (CGPoint)calculateXY:(UIColor *)color forModel:(NSString*)model The color to xy/brightness does not return a value, instead takes two pointers to variables which it will change to the appropriate values. The model parameter of both methods is the modelNumber value of a PHLight object. The advantage of this model being settable is that you can decide if you want to limit the color of all lights to a certain model, or that every light should do the colors within its own range.
  4. Current Philips lights have a color gamut defined by 3 points, making it a triangle.
  5. For the hue bulb the corners of the triangle are:
  6. Red: 0.675, 0.322 Green: 0.4091, 0.518 Blue: 0.167, 0.04 For LivingColors Bloom, Aura and Iris the triangle corners are:
  7. Red: 0.704, 0.296 Green: 0.2151, 0.7106 Blue: 0.138, 0.08 If you have light which is not one of those, you should use:
  8. Red: 1.0, 0 Green: 0.0, 1.0 Blue: 0.0, 0.0
  9. ##Color(RGB) to xy We start with the color to xy conversion, which we will do in a couple of steps: 1. Get the RGB values from your color object and convert them to be between 0 and 1. So the RGB color (255, 0, 100) becomes (1.0, 0.0, 0.39) 2. Apply a gamma correction to the RGB values, which makes the color more vivid and more the like the color displayed on the screen of your device. This gamma correction is also applied to the screen of your computer or phone, thus we need this to create the same color on the light as on screen. This is done by the following formulas: float red = (red > 0.04045f) ? pow((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f);
  10. float green = (green > 0.04045f) ? pow((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f);
  11. float blue = (blue > 0.04045f) ? pow((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f);
  12. 3. Convert the RGB values to XYZ using the Wide RGB D65 conversion formula The formulas used: float X = red * 0.649926f + green * 0.103455f + blue * 0.197109f;
  13. float Y = red * 0.234327f + green * 0.743075f + blue * 0.022598f;
  14. float Z = red * 0.0000000f + green * 0.053077f + blue * 1.035763f;
  15. 4. Calculate the xy values from the XYZ values float x = X / (X + Y + Z); float y = Y / (X + Y + Z);
  16. 5. Check if the found xy value is within the color gamut of the light, if not continue with step 6, otherwise step 7 When we sent a value which the light is not capable of, the resulting color might not be optimal. Therefor we try to only sent values which are inside the color gamut of the selected light.
  17. 6. Calculate the closest point on the color gamut triangle and use that as xy value The closest value is calculated by making a perpendicular line to one of the lines the triangle consists of and when it is then still not inside the triangle, we choose the closest corner point of the triangle. 7. Use the Y value of XYZ as brightness
  18. The Y value indicates the brightness of the converted color. ##xy to color(RGB) The xy to color conversion is almost the same, but in reverse order.
  19. 1. Check if the xy value is within the color gamut of the lamp, if not continue with step 2, otherwise step 3 We do this to calculate the most accurate color the given light can actually do. 2. Calculate the closest point on the color gamut triangle and use that as xy value See step 6 of color to xy. 3. Calculate XYZ values Convert using the following formulas:
  20. float x = x; // the given x value
  21. float y = y; // the given y value
  22. float z = 1.0f - x - y;
  23. float Y = brightness; // The given brightness value
  24. float X = (Y / y) * x;
  25. float Z = (Y / y) * z;
  26. 4. Convert to RGB using Wide RGB D65 conversion
  27. float r = X * 1.612f - Y * 0.203f - Z * 0.302f;
  28. float g = -X * 0.509f + Y * 1.412f + Z * 0.066f;
  29. float b = X * 0.026f - Y * 0.072f + Z * 0.962f;
  30. 5. Apply reverse gamma correction r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * pow(r, (1.0f / 2.4f)) - 0.055f;
  31. g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * pow(g, (1.0f / 2.4f)) - 0.055f;
  32. b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * pow(b, (1.0f / 2.4f)) - 0.055f;
  33. 6. Convert the RGB values to your color object The rgb values from the above formulas are between 0.0 and 1.0.
  34. ##Code Examples
  35. The following code is an extract from the relevant methods in the iOS SDK. It is provided on an as is basis to help you create your own versions of the color conversion utilities.
  36. Note that the UIColor class contains a method to obtain Hue/Saturation values http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIColor_Class/Reference/Reference.html
  37. + (UIColor *)colorFromXY:(CGPoint)xy forModel:(NSString*)model {
  38. NSArray *colorPoints = [self colorPointsForModel:model];
  39. BOOL inReachOfLamps = [self checkPointInLampsReach:xy withColorPoints:colorPoints];
  40. if (!inReachOfLamps) {
  41. //It seems the colour is out of reach
  42. //let's find the closest colour we can produce with our lamp and send this XY value out.
  43. //Find the closest point on each line in the triangle.
  44. CGPoint pAB =[self getClosestPointToPoints:[self getPointFromValue:[colorPoints objectAtIndex:cptRED]] point2:[self getPointFromValue:[colorPoints objectAtIndex:cptGREEN]] point3:xy];
  45. CGPoint pAC = [self getClosestPointToPoints:[self getPointFromValue:[colorPoints objectAtIndex:cptBLUE]] point2:[self getPointFromValue:[colorPoints objectAtIndex:cptRED]] point3:xy];
  46. CGPoint pBC = [self getClosestPointToPoints:[self getPointFromValue:[colorPoints objectAtIndex:cptGREEN]] point2:[self getPointFromValue:[colorPoints objectAtIndex:cptBLUE]] point3:xy];
  47. //Get the distances per point and see which point is closer to our Point.
  48. float dAB = [self getDistanceBetweenTwoPoints:xy point2:pAB];
  49. float dAC = [self getDistanceBetweenTwoPoints:xy point2:pAC];
  50. float dBC = [self getDistanceBetweenTwoPoints:xy point2:pBC];
  51. float lowest = dAB;
  52. CGPoint closestPoint = pAB;
  53. if (dAC < lowest) {
  54. lowest = dAC;
  55. closestPoint = pAC;
  56. }
  57. if (dBC < lowest) {
  58. lowest = dBC;
  59. closestPoint = pBC;
  60. }
  61. //Change the xy value to a value which is within the reach of the lamp.
  62. xy.x = closestPoint.x;
  63. xy.y = closestPoint.y;
  64. }
  65. float x = xy.x;
  66. float y = xy.y;
  67. float z = 1.0f - x - y;
  68. float Y = 1.0f;
  69. float X = (Y / y) * x;
  70. float Z = (Y / y) * z;
  71. // sRGB D65 conversion
  72. float r = X * 3.2406f - Y * 1.5372f - Z * 0.4986f;
  73. float g = -X * 0.9689f + Y * 1.8758f + Z * 0.0415f;
  74. float b = X * 0.0557f - Y * 0.2040f + Z * 1.0570f;
  75. if (r > b && r > g && r > 1.0f) {
  76. // red is too big
  77. g = g / r;
  78. b = b / r;
  79. r = 1.0f;
  80. }
  81. else if (g > b && g > r && g > 1.0f) {
  82. // green is too big
  83. r = r / g;
  84. b = b / g;
  85. g = 1.0f;
  86. }
  87. else if (b > r && b > g && b > 1.0f) {
  88. // blue is too big
  89. r = r / b;
  90. g = g / b;
  91. b = 1.0f;
  92. }
  93. // Apply gamma correction
  94. r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * pow(r, (1.0f / 2.4f)) - 0.055f;
  95. g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * pow(g, (1.0f / 2.4f)) - 0.055f;
  96. b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * pow(b, (1.0f / 2.4f)) - 0.055f;
  97. if (r > b && r > g) {
  98. // red is biggest
  99. if (r > 1.0f) {
  100. g = g / r;
  101. b = b / r;
  102. r = 1.0f;
  103. }
  104. }
  105. else if (g > b && g > r) {
  106. // green is biggest
  107. if (g > 1.0f) {
  108. r = r / g;
  109. b = b / g;
  110. g = 1.0f;
  111. }
  112. }
  113. else if (b > r && b > g) {
  114. // blue is biggest
  115. if (b > 1.0f) {
  116. r = r / b;
  117. g = g / b;
  118. b = 1.0f;
  119. }
  120. }
  121. return [UIColor colorWithRed:r green:g blue:b alpha:1.0f];
  122. }
  123. + (NSArray *)colorPointsForModel:(NSString*)model {
  124. NSMutableArray *colorPoints = [NSMutableArray array];
  125. NSArray *hueBulbs = [NSArray arrayWithObjects:@"LCT001" /* Hue A19 */,
  126. @"LCT002" /* Hue BR30 */,
  127. @"LCT003" /* Hue GU10 */, nil];
  128. NSArray *livingColors = [NSArray arrayWithObjects: @"LLC001" /* Monet, Renoir, Mondriaan (gen II) */,
  129. @"LLC005" /* Bloom (gen II) */,
  130. @"LLC006" /* Iris (gen III) */,
  131. @"LLC007" /* Bloom, Aura (gen III) */,
  132. @"LLC011" /* Hue Bloom */,
  133. @"LLC012" /* Hue Bloom */,
  134. @"LLC013" /* Storylight */,
  135. @"LST001" /* Light Strips */, nil];
  136. if ([hueBulbs containsObject:model]) {
  137. // Hue bulbs color gamut triangle
  138. [colorPoints addObject:[self getValueFromPoint:CGPointMake(0.674F, 0.322F)]]; // Red
  139. [colorPoints addObject:[self getValueFromPoint:CGPointMake(0.408F, 0.517F)]]; // Green
  140. [colorPoints addObject:[self getValueFromPoint:CGPointMake(0.168F, 0.041F)]]; // Blue
  141. }
  142. else if ([livingColors containsObject:model]) {
  143. // LivingColors color gamut triangle
  144. [colorPoints addObject:[self getValueFromPoint:CGPointMake(0.703F, 0.296F)]]; // Red
  145. [colorPoints addObject:[self getValueFromPoint:CGPointMake(0.214F, 0.709F)]]; // Green
  146. [colorPoints addObject:[self getValueFromPoint:CGPointMake(0.139F, 0.081F)]]; // Blue
  147. }
  148. else {
  149. // Default construct triangle wich contains all values
  150. [colorPoints addObject:[self getValueFromPoint:CGPointMake(1.0F, 0.0F)]]; // Red
  151. [colorPoints addObject:[self getValueFromPoint:CGPointMake(0.0F, 1.0F)]]; // Green
  152. [colorPoints addObject:[self getValueFromPoint:CGPointMake(0.0F, 0.0F)]]; // Blue
  153. }
  154. return colorPoints;
  155. }
  156. + (CGPoint)calculateXY:(UIColor *)color forModel:(NSString*)model {
  157. CGColorRef cgColor = [color CGColor];
  158. const CGFloat *components = CGColorGetComponents(cgColor);
  159. long numberOfComponents = CGColorGetNumberOfComponents(cgColor);
  160. // Default to white
  161. CGFloat red = 1.0f;
  162. CGFloat green = 1.0f;
  163. CGFloat blue = 1.0f;
  164. if (numberOfComponents == 4) {
  165. // Full color
  166. red = components[0];
  167. green = components[1];
  168. blue = components[2];
  169. }
  170. else if (numberOfComponents == 2) {
  171. // Greyscale color
  172. red = green = blue = components[0];
  173. }
  174. // Apply gamma correction
  175. float r = (red > 0.04045f) ? pow((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f);
  176. float g = (green > 0.04045f) ? pow((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f);
  177. float b = (blue > 0.04045f) ? pow((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f);
  178. // Wide gamut conversion D65
  179. float X = r * 0.649926f + g * 0.103455f + b * 0.197109f;
  180. float Y = r * 0.234327f + g * 0.743075f + b * 0.022598f;
  181. float Z = r * 0.0000000f + g * 0.053077f + b * 1.035763f;
  182. float cx = X / (X + Y + Z);
  183. float cy = Y / (X + Y + Z);
  184. if (isnan(cx)) {
  185. cx = 0.0f;
  186. }
  187. if (isnan(cy)) {
  188. cy = 0.0f;
  189. }
  190. //Check if the given XY value is within the colourreach of our lamps.
  191. CGPoint xyPoint = CGPointMake(cx,cy);
  192. NSArray *colorPoints = [self colorPointsForModel:model];
  193. BOOL inReachOfLamps = [self checkPointInLampsReach:xyPoint withColorPoints:colorPoints];
  194. if (!inReachOfLamps) {
  195. //It seems the colour is out of reach
  196. //let's find the closest colour we can produce with our lamp and send this XY value out.
  197. //Find the closest point on each line in the triangle.
  198. CGPoint pAB =[self getClosestPointToPoints:[self getPointFromValue:[colorPoints objectAtIndex:cptRED]] point2:[self getPointFromValue:[colorPoints objectAtIndex:cptGREEN]] point3:xyPoint];
  199. CGPoint pAC = [self getClosestPointToPoints:[self getPointFromValue:[colorPoints objectAtIndex:cptBLUE]] point2:[self getPointFromValue:[colorPoints objectAtIndex:cptRED]] point3:xyPoint];
  200. CGPoint pBC = [self getClosestPointToPoints:[self getPointFromValue:[colorPoints objectAtIndex:cptGREEN]] point2:[self getPointFromValue:[colorPoints objectAtIndex:cptBLUE]] point3:xyPoint];
  201. //Get the distances per point and see which point is closer to our Point.
  202. float dAB = [self getDistanceBetweenTwoPoints:xyPoint point2:pAB];
  203. float dAC = [self getDistanceBetweenTwoPoints:xyPoint point2:pAC];
  204. float dBC = [self getDistanceBetweenTwoPoints:xyPoint point2:pBC];
  205. float lowest = dAB;
  206. CGPoint closestPoint = pAB;
  207. if (dAC < lowest) {
  208. lowest = dAC;
  209. closestPoint = pAC;
  210. }
  211. if (dBC < lowest) {
  212. lowest = dBC;
  213. closestPoint = pBC;
  214. }
  215. //Change the xy value to a value which is within the reach of the lamp.
  216. cx = closestPoint.x;
  217. cy = closestPoint.y;
  218. }
  219. return CGPointMake(cx, cy);
  220. }
  221. /**
  222. * Calculates crossProduct of two 2D vectors / points.
  223. *
  224. * @param p1 first point used as vector
  225. * @param p2 second point used as vector
  226. * @return crossProduct of vectors
  227. */
  228. + (float)crossProduct:(CGPoint)p1 point2:(CGPoint)p2 {
  229. return (p1.x * p2.y - p1.y * p2.x);
  230. }
  231. /**
  232. * Find the closest point on a line.
  233. * This point will be within reach of the lamp.
  234. *
  235. * @param A the point where the line starts
  236. * @param B the point where the line ends
  237. * @param P the point which is close to a line.
  238. * @return the point which is on the line.
  239. */
  240. + (CGPoint)getClosestPointToPoints:(CGPoint)A point2:(CGPoint)B point3:(CGPoint)P {
  241. CGPoint AP = CGPointMake(P.x - A.x, P.y - A.y);
  242. CGPoint AB = CGPointMake(B.x - A.x, B.y - A.y);
  243. float ab2 = AB.x * AB.x + AB.y * AB.y;
  244. float ap_ab = AP.x * AB.x + AP.y * AB.y;
  245. float t = ap_ab / ab2;
  246. if (t < 0.0f) {
  247. t = 0.0f;
  248. }
  249. else if (t > 1.0f) {
  250. t = 1.0f;
  251. }
  252. CGPoint newPoint = CGPointMake(A.x + AB.x * t, A.y + AB.y * t);
  253. return newPoint;
  254. }
  255. /**
  256. * Find the distance between two points.
  257. *
  258. * @param one
  259. * @param two
  260. * @return the distance between point one and two
  261. */
  262. + (float)getDistanceBetweenTwoPoints:(CGPoint)one point2:(CGPoint)two {
  263. float dx = one.x - two.x; // horizontal difference
  264. float dy = one.y - two.y; // vertical difference
  265. float dist = sqrt(dx * dx + dy * dy);
  266. return dist;
  267. }
  268. /**
  269. * Method to see if the given XY value is within the reach of the lamps.
  270. *
  271. * @param p the point containing the X,Y value
  272. * @return true if within reach, false otherwise.
  273. */
  274. + (BOOL)checkPointInLampsReach:(CGPoint)p withColorPoints:(NSArray*)colorPoints {
  275. CGPoint red = [self getPointFromValue:[colorPoints objectAtIndex:cptRED]];
  276. CGPoint green = [self getPointFromValue:[colorPoints objectAtIndex:cptGREEN]];
  277. CGPoint blue = [self getPointFromValue:[colorPoints objectAtIndex:cptBLUE]];
  278. CGPoint v1 = CGPointMake(green.x - red.x, green.y - red.y);
  279. CGPoint v2 = CGPointMake(blue.x - red.x, blue.y - red.y);
  280. CGPoint q = CGPointMake(p.x - red.x, p.y - red.y);
  281. float s = [self crossProduct:q point2:v2] / [self crossProduct:v1 point2:v2];
  282. float t = [self crossProduct:v1 point2:q] / [self crossProduct:v1 point2:v2];
  283. if ( (s >= 0.0f) && (t >= 0.0f) && (s + t <= 1.0f)) {
  284. return true;
  285. }
  286. else {
  287. return false;
  288. }
  289. }
  290. ##Further Information
  291. The following links provide further useful related information
  292. sRGB:
  293. http://en.wikipedia.org/wiki/Srgb
  294. A Review of RGB Color Spaces:
  295. http://www.babelcolor.com/download/A%20review%20of%20RGB%20color%20spaces.pdf
  296. Useful color equations:
  297. http://www.brucelindbloom.com/