[core] Add conversion for HSB to RGBW and back (#3849)

* Add conversion for HSB to RGBW and back
for KNX DPT251.600 to use all 4 colors.

With the new feature, the HSBType can converted into RGBW, and also back
to HSB.

Due to the conversion, some accuracy is lost, but the result is
approximately correct.

Signed-off-by: Marco Müller <marco@ms-mueller.ch>
This commit is contained in:
Marco Müller 2023-11-12 11:23:44 +01:00 committed by GitHub
parent 81c1dcc4ab
commit 237f2ebb56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 189 additions and 8 deletions

View File

@ -44,8 +44,10 @@ public class ColorUtil {
private static final BigDecimal BIG_DECIMAL_120 = BigDecimal.valueOf(120);
private static final BigDecimal BIG_DECIMAL_100 = BigDecimal.valueOf(100);
private static final BigDecimal BIG_DECIMAL_60 = BigDecimal.valueOf(60);
private static final BigDecimal BIG_DECIMAL_50 = BigDecimal.valueOf(50);
private static final BigDecimal BIG_DECIMAL_5 = BigDecimal.valueOf(5);
private static final BigDecimal BIG_DECIMAL_3 = BigDecimal.valueOf(3);
private static final BigDecimal BIG_DECIMAL_2 = BigDecimal.valueOf(2);
private static final BigDecimal BIG_DECIMAL_2_POINT_55 = new BigDecimal("2.55");
public static final Gamut DEFAULT_GAMUT = new Gamut(new double[] { 0.9961, 0.0001 }, new double[] { 0, 0.9961 },
@ -61,7 +63,7 @@ public class ColorUtil {
*
* This function does rounding to integer valued components. It is the preferred way of doing HSB to RGB conversion.
*
* See also: {@link #hsbToRgbPercent(HSBType)}, {@link #hsbTosRgb(HSBType)}
* See also: {@link #hsbToRgbPercent(HSBType)}, {@link #hsbToRgbw(HSBType)}, {@link #hsbTosRgb(HSBType)}
*
* @param hsb an {@link HSBType} value.
* @return array of three int with the RGB values in the range 0 to 255.
@ -72,6 +74,24 @@ public class ColorUtil {
convertColorPercentToByte(rgbPercent[2]) };
}
/**
* Transform <a href="https://en.wikipedia.org/wiki/HSL_and_HSV">HSV</a> based {@link HSBType} to
* <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a>.
*
* This function does rounding to integer valued components. It is the preferred way of doing HSB to RGBW
* conversion.
*
* See also: {@link #hsbToRgbPercent(HSBType)}, {@link #hsbToRgbwPercent(HSBType)}, {@link #hsbTosRgb(HSBType)}
*
* @param hsb an {@link HSBType} value.
* @return array of four int with the RGBW values in the range 0 to 255.
*/
public static int[] hsbToRgbw(HSBType hsb) {
final PercentType[] rgbPercent = hsbToRgbwPercent(hsb);
return new int[] { convertColorPercentToByte(rgbPercent[0]), convertColorPercentToByte(rgbPercent[1]),
convertColorPercentToByte(rgbPercent[2]), convertColorPercentToByte(rgbPercent[3]) };
}
/**
* Transform <a href="https://en.wikipedia.org/wiki/HSL_and_HSV">HSV</a> based {@link HSBType} to
* <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a>.
@ -79,7 +99,7 @@ public class ColorUtil {
* This function does not round the components. For conversion to integer values in the range 0 to 255 use
* {@link #hsbToRgb(HSBType)}.
*
* See also: {@link #hsbToRgb(HSBType)}, {@link #hsbTosRgb(HSBType)}
* See also: {@link #hsbToRgb(HSBType)}, {@link #hsbTosRgb(HSBType)}, {@link #hsbToRgbwPercent(HSBType)}
*
* @param hsb an {@link HSBType} value.
* @return array of three {@link PercentType} with the RGB values in the range 0 to 100 percent.
@ -140,6 +160,55 @@ public class ColorUtil {
return new PercentType[] { red, green, blue };
}
/**
* Transform <a href="https://en.wikipedia.org/wiki/HSL_and_HSV">HSV</a> based {@link HSBType} to RGBW.
*
* See <a href=
* "https://stackoverflow.com/questions/40312216/converting-rgb-to-rgbw">Converting RGB to RGBW</a>.
*
* This function does not round the components. For conversion to integer values in the range 0 to 255 use
* {@link #hsbToRgb(HSBType)}.
*
* See also: {@link #hsbToRgb(HSBType)}, {@link #hsbTosRgb(HSBType)}, {@link #hsbToRgbPercent(HSBType)}
*
* @param hsb an {@link HSBType} value.
* @return array of four {@link PercentType} with the RGBW values in the range 0 to 100 percent.
*/
public static PercentType[] hsbToRgbwPercent(HSBType hsb) {
PercentType[] rgb = hsbToRgbPercent(hsb);
final BigDecimal inRed = rgb[0].toBigDecimal();
final BigDecimal inGreen = rgb[1].toBigDecimal();
final BigDecimal inBlue = rgb[2].toBigDecimal();
// Get the maximum between R, G, and B
final BigDecimal maxColor = inRed.max(inGreen.max(inBlue));
// If the maximum value is 0, immediately return pure black.
if (BigDecimal.ZERO.equals(maxColor)) {
return new PercentType[] { PercentType.ZERO, PercentType.ZERO, PercentType.ZERO, PercentType.ZERO };
}
// This section serves to figure out what the color with 100% hue is
final BigDecimal multiplier = BIG_DECIMAL_100.divide(maxColor, 0, RoundingMode.DOWN);
final BigDecimal hR = inRed.multiply(multiplier);
final BigDecimal hG = inGreen.multiply(multiplier);
final BigDecimal hB = inBlue.multiply(multiplier);
// This calculates the Whiteness (not strictly speaking Luminance) of the color
final BigDecimal whitenessMax = hR.max(hG.max(hB));
final BigDecimal whitenessMin = hR.min(hG.min(hB));
final BigDecimal luminance = ((whitenessMax.add(whitenessMin).divide(BIG_DECIMAL_2).subtract(BIG_DECIMAL_50))
.multiply(BIG_DECIMAL_100.divide(BIG_DECIMAL_50))).divide(multiplier);
// check range
BigDecimal outRed = inRed.subtract(luminance).max(BigDecimal.ZERO);
BigDecimal outGreen = inGreen.subtract(luminance).max(BigDecimal.ZERO);
BigDecimal outBlue = inBlue.subtract(luminance).max(BigDecimal.ZERO);
BigDecimal outWhite = luminance.max(BigDecimal.ZERO);
return new PercentType[] { new PercentType(outRed), new PercentType(outGreen), new PercentType(outBlue),
new PercentType(outWhite) };
}
/**
* Transform <a href="https://en.wikipedia.org/wiki/HSL_and_HSV">HSV</a> based {@link HSBType}
* to the RGB value representing the color in the default
@ -211,23 +280,69 @@ public class ColorUtil {
* Transform <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a> color format to
* <a href="https://en.wikipedia.org/wiki/HSL_and_HSV">HSV</a> based {@link HSBType}.
*
* @param rgb array of three int with the RGB values in the range 0 to 255.
* @param rgbw array of three or four int with the RGB(W) values in the range 0 to 255.
* @return the corresponding {@link HSBType}.
* @throws IllegalArgumentException when input array has wrong size or exceeds allowed value range.
*/
public static HSBType rgbToHsb(int[] rgb) throws IllegalArgumentException {
if (rgb.length != 3 || !inByteRange(rgb[0]) || !inByteRange(rgb[1]) || !inByteRange(rgb[2])) {
public static HSBType rgbToHsb(int[] rgbw) throws IllegalArgumentException {
if (rgbw.length == 4) {
return rgbwToHsb(rgbw);
}
if (rgbw.length != 3 || !inByteRange(rgbw[0]) || !inByteRange(rgbw[1]) || !inByteRange(rgbw[2])) {
throw new IllegalArgumentException("RGB array only allows values between 0 and 255");
}
return rgbToHsb(new PercentType[] { convertByteToColorPercent(rgb[0]), convertByteToColorPercent(rgb[1]),
convertByteToColorPercent(rgb[2]) });
return rgbToHsb(new PercentType[] { convertByteToColorPercent(rgbw[0]), convertByteToColorPercent(rgbw[1]),
convertByteToColorPercent(rgbw[2]) });
}
/**
* Transform <a href="https://en.wikipedia.org/wiki/HSL_and_HSV">HSV</a> based {@link HSBType} to RGBW.
*
* See <a href=
* "https://stackoverflow.com/questions/40312216/converting-rgb-to-rgbw">Converting RGB to RGBW</a>.
*
* This function does not round the components. For conversion to integer values in the range 0 to 255 use
* {@link #hsbToRgb(HSBType)}.
*
* See also: {@link #hsbToRgb(HSBType)}, {@link #hsbTosRgb(HSBType)}, {@link #hsbToRgbPercent(HSBType)}
*
* @param rgbw array of four int with the RGBW values in the range 0 to 255.
* @return hsb an {@link HSBType} value.
*
*/
private static HSBType rgbwToHsb(int[] rgbw) {
if (rgbw.length != 4 || !inByteRange(rgbw[0]) || !inByteRange(rgbw[1]) || !inByteRange(rgbw[2])
|| !inByteRange(rgbw[3])) {
throw new IllegalArgumentException("RGBW array only allows values between 0 and 255 with 4 values");
}
BigDecimal luminance = BigDecimal.valueOf(rgbw[3]);
BigDecimal inRed = BigDecimal.valueOf(rgbw[0]).add(luminance);
BigDecimal inGreen = BigDecimal.valueOf(rgbw[1]).add(luminance);
BigDecimal inBlue = BigDecimal.valueOf(rgbw[2]).add(luminance);
// Get the maximum between R, G, and B
final BigDecimal maxColor = BIG_DECIMAL_255.min(inRed.max(inGreen.max(inBlue)).max(BigDecimal.ZERO));
// If the maximum value is 0, immediately return pure black.
if (BigDecimal.ZERO.compareTo(maxColor) == 0) {
return HSBType.BLACK;
}
final BigDecimal multiplier = BIG_DECIMAL_255.divide(maxColor, 0, RoundingMode.DOWN);
BigDecimal outRed = inRed.divide(multiplier).min(BIG_DECIMAL_255).max(BigDecimal.ZERO);
BigDecimal outGreen = inGreen.divide(multiplier).min(BIG_DECIMAL_255).max(BigDecimal.ZERO);
BigDecimal outBlue = inBlue.divide(multiplier).min(BIG_DECIMAL_255).max(BigDecimal.ZERO);
return HSBType.fromRGB(outRed.intValue(), outGreen.intValue(), outBlue.intValue());
}
/**
* Transform <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a> color format to
* <a href="https://en.wikipedia.org/wiki/HSL_and_HSV">HSV</a> based {@link HSBType}.
*
* @param rgb array of three {@link PercentType] with the RGB values in the range 0 to 100 percent.
* @param rgb array of three {@link PercentType} with the RGB values in the range 0 to 100 percent.
* @return the corresponding {@link HSBType}.
* @throws IllegalArgumentException when input array has wrong size or exceeds allowed value range.
*/

View File

@ -59,6 +59,72 @@ public class ColorUtilTest {
assertThat(deltaBri, is(lessThanOrEqualTo(1.0)));
}
@Test
public void hsbToRgbwTest() {
HSBType hsb = HSBType.WHITE;
PercentType[] rgbw = ColorUtil.hsbToRgbwPercent(hsb);
assertEquals(0.0, rgbw[0].doubleValue(), 0.01);
assertEquals(0.0, rgbw[1].doubleValue(), 0.01);
assertEquals(0.0, rgbw[2].doubleValue(), 0.01);
assertEquals(100.0, rgbw[3].doubleValue(), 0.01);
hsb = HSBType.BLACK;
rgbw = ColorUtil.hsbToRgbwPercent(hsb);
assertEquals(0.0, rgbw[0].doubleValue(), 0.01);
assertEquals(0.0, rgbw[1].doubleValue(), 0.01);
assertEquals(0.0, rgbw[2].doubleValue(), 0.01);
assertEquals(0.0, rgbw[3].doubleValue(), 0.01);
hsb = HSBType.RED;
rgbw = ColorUtil.hsbToRgbwPercent(hsb);
assertEquals(100.0, rgbw[0].doubleValue(), 0.01);
assertEquals(0.0, rgbw[1].doubleValue(), 0.01);
assertEquals(0.0, rgbw[2].doubleValue(), 0.01);
assertEquals(0.0, rgbw[3].doubleValue(), 0.01);
hsb = HSBType.GREEN;
rgbw = ColorUtil.hsbToRgbwPercent(hsb);
assertEquals(0.0, rgbw[0].doubleValue(), 0.01);
assertEquals(100.0, rgbw[1].doubleValue(), 0.01);
assertEquals(0.0, rgbw[2].doubleValue(), 0.01);
assertEquals(0.0, rgbw[3].doubleValue(), 0.01);
hsb = HSBType.BLUE;
rgbw = ColorUtil.hsbToRgbwPercent(hsb);
assertEquals(0.0, rgbw[0].doubleValue(), 0.01);
assertEquals(0.0, rgbw[1].doubleValue(), 0.01);
assertEquals(100.0, rgbw[2].doubleValue(), 0.01);
assertEquals(0.0, rgbw[3].doubleValue(), 0.01);
}
@Test
public void rgbwToHsbTest() {
// Test Red
HSBType hsb = ColorUtil.rgbToHsb(new int[] { 255, 0, 0, 0 });
int[] convertedRgb = ColorUtil.hsbToRgb(hsb);
assertRgbEquals(new int[] { 255, 0, 0 }, convertedRgb);
// Test Green
hsb = ColorUtil.rgbToHsb(new int[] { 0, 255, 0, 0 });
convertedRgb = ColorUtil.hsbToRgb(hsb);
assertRgbEquals(new int[] { 0, 255, 0 }, convertedRgb);
// Test Blue
hsb = ColorUtil.rgbToHsb(new int[] { 0, 0, 255, 0 });
convertedRgb = ColorUtil.hsbToRgb(hsb);
assertRgbEquals(new int[] { 0, 0, 255 }, convertedRgb);
// Test White
hsb = ColorUtil.rgbToHsb(new int[] { 0, 0, 0, 255 });
convertedRgb = ColorUtil.hsbToRgb(hsb);
assertRgbEquals(new int[] { 255, 255, 255 }, convertedRgb);
// Test Black
hsb = ColorUtil.rgbToHsb(new int[] { 0, 0, 0, 0 });
convertedRgb = ColorUtil.hsbToRgb(hsb);
assertRgbEquals(new int[] { 0, 0, 0 }, convertedRgb);
}
@ParameterizedTest
@MethodSource("invalids")
public void invalidXyValues(double[] xy) {