Reduce rounding errors of RGB/HSB conversion and enhance ColorUtil (#3479)

* HSBType: Reduce rounding errors of RGB/HSB conversion
* Move RGB to HSV conversion to ColorUtil
* Restructuring HSBType and ColorUtil

- Move RBG/HSB conversion from HSBType to ColorUtil
- Rename helper functions "hsv" to "hsb" to be consistent with HSBType
- Add parameterized tests

Co-authored-by: Jacob Laursen <jacob-github@vindvejr.dk>
Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
This commit is contained in:
Holger Friedrich 2023-04-07 18:43:01 +02:00 committed by GitHub
parent 3047ed42a5
commit b0b8bb547b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 409 additions and 139 deletions

View File

@ -34,6 +34,7 @@ import org.openhab.core.util.ColorUtil;
*
* @author Kai Kreuzer - Initial contribution
* @author Chris Jackson - Added fromRGB
* @author Andrew Fiddian-Green - closeTo (copied from binding)
*/
@NonNullByDefault
public class HSBType extends PercentType implements ComplexType, State, Command {
@ -113,50 +114,22 @@ public class HSBType extends PercentType implements ComplexType, State, Command
}
/**
* Create HSB from RGB
* Create HSB from RGB.
*
* See also {@link ColorUtil#rgbToHsb(int[])}.
*
* @param r red 0-255
* @param g green 0-255
* @param b blue 0-255
* @throws IllegalArgumentException when color values exceed allowed range
*/
public static HSBType fromRGB(int r, int g, int b) {
float tmpHue, tmpSaturation, tmpBrightness;
int max = (r > g) ? r : g;
if (b > max) {
max = b;
}
int min = (r < g) ? r : g;
if (b < min) {
min = b;
}
tmpBrightness = max / 2.55f;
tmpSaturation = (max != 0 ? ((float) (max - min)) / ((float) max) : 0) * 100;
if (tmpSaturation == 0) {
tmpHue = 0;
} else {
float red = ((float) (max - r)) / ((float) (max - min));
float green = ((float) (max - g)) / ((float) (max - min));
float blue = ((float) (max - b)) / ((float) (max - min));
if (r == max) {
tmpHue = blue - green;
} else if (g == max) {
tmpHue = 2.0f + red - blue;
} else {
tmpHue = 4.0f + green - red;
}
tmpHue = tmpHue / 6.0f * 360;
if (tmpHue < 0) {
tmpHue = tmpHue + 360.0f;
}
}
return new HSBType(new DecimalType((int) tmpHue), new PercentType((int) tmpSaturation),
new PercentType((int) tmpBrightness));
public static HSBType fromRGB(int r, int g, int b) throws IllegalArgumentException {
return ColorUtil.rgbToHsb(new int[] { r, g, b });
}
/**
* @deprecated Use {@link ColorUtil#xyToHsv(double[])} or {@link ColorUtil#xyToHsv(double[], ColorUtil.Gamut)}
* instead
* @deprecated Use {@link ColorUtil#xyToHsb(double[])} or {@link ColorUtil#xyToHsb(double[], ColorUtil.Gamut)}
* instead.
*
* Returns a HSBType object representing the provided xy color values in CIE XY color model.
* Conversion from CIE XY color model to sRGB using D65 reference white
@ -164,11 +137,11 @@ public class HSBType extends PercentType implements ComplexType, State, Command
*
* @param x, y color information 0.0 - 1.0
* @return new HSBType object representing the given CIE XY color, full brightness
*
* @throws IllegalArgumentException when input array has wrong size or exceeds allowed value range
*/
@Deprecated
public static HSBType fromXY(float x, float y) {
return ColorUtil.xyToHsv(new double[] { x, y });
public static HSBType fromXY(float x, float y) throws IllegalArgumentException {
return ColorUtil.xyToHsb(new double[] { x, y });
}
@Override
@ -192,29 +165,36 @@ public class HSBType extends PercentType implements ComplexType, State, Command
return new PercentType(value);
}
/** @deprecated Use {@link ColorUtil#hsbToRgb(HSBType)} instead */
@Deprecated
public PercentType getRed() {
return toRGB()[0];
}
/** @deprecated Use {@link ColorUtil#hsbToRgb(HSBType)} instead */
@Deprecated
public PercentType getGreen() {
return toRGB()[1];
}
/** @deprecated Use {@link ColorUtil#hsbToRgb(HSBType)} instead */
@Deprecated
public PercentType getBlue() {
return toRGB()[2];
}
/**
* Returns the RGB value representing the color in the default sRGB
* color model.
* (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue).
* @deprecated Use {@link ColorUtil#hsbTosRgb(HSBType)} instead.
*
* Returns the RGB value representing the color in the default sRGB
* color model.
* (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue).
*
* @return the RGB value of the color in the default sRGB color model
*/
@Deprecated
public int getRGB() {
PercentType[] rgb = toRGB();
return ((0xFF) << 24) | ((convertPercentToByte(rgb[0]) & 0xFF) << 16)
| ((convertPercentToByte(rgb[1]) & 0xFF) << 8) | ((convertPercentToByte(rgb[2]) & 0xFF) << 0);
return ColorUtil.hsbTosRgb(this);
}
@Override
@ -266,58 +246,10 @@ public class HSBType extends PercentType implements ComplexType, State, Command
&& getBrightness().equals(other.getBrightness());
}
/* @deprecated Use {@link ColorUtil#hsbToRgb(HSBType)} instead */
@Deprecated
public PercentType[] toRGB() {
PercentType red = null;
PercentType green = null;
PercentType blue = null;
BigDecimal h = hue.divide(BIG_DECIMAL_HUNDRED, 10, RoundingMode.HALF_UP);
BigDecimal s = saturation.divide(BIG_DECIMAL_HUNDRED);
int hInt = h.multiply(BigDecimal.valueOf(5)).divide(BigDecimal.valueOf(3), 10, RoundingMode.HALF_UP).intValue();
BigDecimal f = h.multiply(BigDecimal.valueOf(5)).divide(BigDecimal.valueOf(3), 10, RoundingMode.HALF_UP)
.remainder(BigDecimal.ONE);
PercentType a = new PercentType(value.multiply(BigDecimal.ONE.subtract(s)));
PercentType b = new PercentType(value.multiply(BigDecimal.ONE.subtract(s.multiply(f))));
PercentType c = new PercentType(
value.multiply(BigDecimal.ONE.subtract((BigDecimal.ONE.subtract(f)).multiply(s))));
switch (hInt) {
case 0:
case 6:
red = getBrightness();
green = c;
blue = a;
break;
case 1:
red = b;
green = getBrightness();
blue = a;
break;
case 2:
red = a;
green = getBrightness();
blue = c;
break;
case 3:
red = a;
green = b;
blue = getBrightness();
break;
case 4:
red = c;
green = a;
blue = getBrightness();
break;
case 5:
red = getBrightness();
green = a;
blue = b;
break;
default:
throw new IllegalArgumentException("Could not convert to RGB.");
}
return new PercentType[] { red, green, blue };
return ColorUtil.hsbToRgbPercent(this);
}
/**
@ -334,7 +266,7 @@ public class HSBType extends PercentType implements ComplexType, State, Command
}
private int convertPercentToByte(PercentType percent) {
return percent.value.multiply(BigDecimal.valueOf(255)).divide(BIG_DECIMAL_HUNDRED, 2, RoundingMode.HALF_UP)
return percent.value.multiply(BigDecimal.valueOf(255)).divide(BIG_DECIMAL_HUNDRED, 0, RoundingMode.HALF_UP)
.intValue();
}
@ -352,4 +284,21 @@ public class HSBType extends PercentType implements ComplexType, State, Command
return defaultConversion(target);
}
}
/**
* Helper method for checking if two HSBType colors are close to each other. A maximum deviation is specifid in
* percent.
*
* @param other an HSBType containing the other color.
* @param maxPercentage the maximum allowed difference in percent (range 0.0..1.0).
* @throws IllegalArgumentException if percentage is out of range.
*/
public boolean closeTo(HSBType other, double maxPercentage) throws IllegalArgumentException {
if (maxPercentage <= 0.0 || maxPercentage > 1.0) {
throw new IllegalArgumentException("'maxPercentage' out of bounds, allowed range 0..1");
}
double[] exp = ColorUtil.hsbToXY(this);
double[] act = ColorUtil.hsbToXY(other);
return ((Math.abs(exp[0] - act[0]) < maxPercentage) && (Math.abs(exp[1] - act[1]) < maxPercentage));
}
}

View File

@ -12,23 +12,31 @@
*/
package org.openhab.core.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ColorUtil} is responsible for converting HSB to CIE
* The {@link ColorUtil} is responsible for converting different color formats.
*
* The class is based work from Erik Baauw for the <a href="https://github.com/ebaauw/homebridge-lib">Homebridge</a>
* project
* The implementation of HSB/CIE conversion is based work from Erik Baauw for the
* <a href="https://github.com/ebaauw/homebridge-lib">Homebridge</a>
* project.
*
* @author Jan N. Klug - Initial contribution
* @author Holger Friedrich - Transfer RGB color conversion from HSBType, improve RGB conversion, restructuring
* @author Chris Jackson - Added fromRGB (moved from HSBType)
*/
@NonNullByDefault
public class ColorUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ColorUtil.class);
protected static final BigDecimal BIG_DECIMAL_HUNDRED = BigDecimal.valueOf(100);
public static final Gamut DEFAULT_GAMUT = new Gamut(new double[] { 0.9961, 0.0001 }, new double[] { 0, 0.9961 },
new double[] { 0, 0.0001 });
@ -37,7 +45,100 @@ public class ColorUtil {
}
/**
* Transform <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a> based {@link HSBType} to
* 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 RGB conversion.
*
* See also: {@link hsbToRgbPercent(HSBType)}, {@link hsbTosRGB(HSBType)}
*/
public static int[] hsbToRgb(HSBType hsb) {
final PercentType[] rgbPercent = hsbToRgbPercent(hsb);
return new int[] { convertColorPercentToByte(rgbPercent[0]), convertColorPercentToByte(rgbPercent[1]),
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 not round the components to integer values. Please consider consider
* using {@link hsbToRgb(HSBType)} whenever integer values are required.
*
* See also: {@link hsbToRgb(HSBType)}, {@link hsbTosRgb(HSBType)}
*/
public static PercentType[] hsbToRgbPercent(HSBType hsb) {
PercentType red = null;
PercentType green = null;
PercentType blue = null;
final BigDecimal h = hsb.getHue().toBigDecimal().divide(BIG_DECIMAL_HUNDRED, 10, RoundingMode.HALF_UP);
final BigDecimal s = hsb.getSaturation().toBigDecimal().divide(BIG_DECIMAL_HUNDRED);
int hInt = h.multiply(BigDecimal.valueOf(5)).divide(BigDecimal.valueOf(3), 0, RoundingMode.DOWN).intValue();
final BigDecimal f = h.multiply(BigDecimal.valueOf(5)).divide(BigDecimal.valueOf(3), 10, RoundingMode.HALF_UP)
.remainder(BigDecimal.ONE);
final BigDecimal value = hsb.getBrightness().toBigDecimal();
PercentType a = new PercentType(value.multiply(BigDecimal.ONE.subtract(s)));
PercentType b = new PercentType(value.multiply(BigDecimal.ONE.subtract(s.multiply(f))));
PercentType c = new PercentType(
value.multiply(BigDecimal.ONE.subtract((BigDecimal.ONE.subtract(f)).multiply(s))));
switch (hInt) {
case 0:
case 6:
red = hsb.getBrightness();
green = c;
blue = a;
break;
case 1:
red = b;
green = hsb.getBrightness();
blue = a;
break;
case 2:
red = a;
green = hsb.getBrightness();
blue = c;
break;
case 3:
red = a;
green = b;
blue = hsb.getBrightness();
break;
case 4:
red = c;
green = a;
blue = hsb.getBrightness();
break;
case 5:
red = hsb.getBrightness();
green = a;
blue = b;
break;
default:
throw new IllegalArgumentException("Could not convert to RGB.");
}
return new PercentType[] { red, green, blue };
}
/**
* 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
* <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a> color model.
* (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue).
*
* See also: {@link hsbToRgb(HSBType)}, {@link hsbToRgbPercent(HSBType)}
*
* @return the RGB value of the color in the default sRGB color model
*/
public static int hsbTosRgb(HSBType hsb) {
final int[] rgb = hsbToRgb(hsb);
return (0xFF << 24) | ((rgb[0] & 0xFF) << 16) | ((rgb[1] & 0xFF) << 8) | ((rgb[2] & 0xFF) << 0);
}
/**
* Transform <a href="https://en.wikipedia.org/wiki/HSL_and_HSV">HSV</a> based {@link HSBType} to
* <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space">CIE 1931</a> `xy` format.
*
* See <a href=
@ -45,14 +146,14 @@ public class ColorUtil {
* developerportal</a>.
*
* @param hsbType a {@link HSBType} value
* @return double array with the closest matching CIE 1931 colour, x, y between 0.0000 and 1.0000.
* @return double array with the closest matching CIE 1931 color, x, y between 0.0000 and 1.0000.
*/
public static double[] hsbToXY(HSBType hsbType) {
return hsbToXY(hsbType, DEFAULT_GAMUT);
}
/**
* Transform <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a> based {@link HSBType} to
* Transform <a href="https://en.wikipedia.org/wiki/HSL_and_HSV">HSV</a> based {@link HSBType} to
* <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space">CIE 1931</a> `xy` format.
*
* See <a href=
@ -61,12 +162,13 @@ public class ColorUtil {
*
* @param hsbType a {@link HSBType} value
* @param gamut the gamut supported by the light.
* @return double array with the closest matching CIE 1931 colour, x, y, Y between 0.0000 and 1.0000.
* @return double array with the closest matching CIE 1931 color, x, y, Y between 0.0000 and 1.0000.
*/
public static double[] hsbToXY(HSBType hsbType, Gamut gamut) {
double r = inverseCompand(hsbType.getRed().doubleValue() / PercentType.HUNDRED.doubleValue());
double g = inverseCompand(hsbType.getGreen().doubleValue() / PercentType.HUNDRED.doubleValue());
double b = inverseCompand(hsbType.getBlue().doubleValue() / PercentType.HUNDRED.doubleValue());
PercentType[] rgb = hsbToRgbPercent(hsbType);
double r = inverseCompand(rgb[0].doubleValue() / PercentType.HUNDRED.doubleValue());
double g = inverseCompand(rgb[1].doubleValue() / PercentType.HUNDRED.doubleValue());
double b = inverseCompand(rgb[2].doubleValue() / PercentType.HUNDRED.doubleValue());
double X = r * 0.664511 + g * 0.154324 + b * 0.162028;
double Y = r * 0.283881 + g * 0.668433 + b * 0.047685;
@ -79,40 +181,97 @@ public class ColorUtil {
double[] xyY = new double[] { ((int) (q.x * 10000.0)) / 10000.0, ((int) (q.y * 10000.0)) / 10000.0,
((int) (Y * 10000.0)) / 10000.0 };
LOGGER.trace("HSV: {} - RGB: {} - XYZ: {} {} {} - xyY: {}", hsbType, hsbType.toRGB(), X, Y, Z, xyY);
LOGGER.trace("HSB: {} - RGB: {} - XYZ: {} {} {} - xyY: {}", hsbType, hsbType.toRGB(), X, Y, Z, xyY);
return xyY;
}
/**
* Transform <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space">CIE 1931</a> `xy` format to
* <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a> based {@link HSBType}.
* 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}.
*
* See <a href=
* "https://developers.meethue.com/develop/application-design-guidance/color-conversion-formulas-rgb-to-xy-and-back/">Hue
* developer portal</a>.
* Note: Conversion result is rounded and HSBType is created with integer valued components.
*
* @param xy the CIE 1931 xy colour, x,y between 0.0000 and 1.0000.
* @param rgb int array of length 3, all values are constrained to 0-255
* @return the corresponding {@link HSBType}.
* @throws IllegalArgumentException when input array has wrong size or exceeds allowed value range
*/
public static HSBType xyToHsv(double[] xy) {
return xyToHsv(xy, DEFAULT_GAMUT);
public static HSBType rgbToHsb(int[] rgb) throws IllegalArgumentException {
if (rgb.length != 3 || !inByteRange(rgb[0]) || !inByteRange(rgb[1]) || !inByteRange(rgb[2])) {
throw new IllegalArgumentException("RGB array only allows values between 0 and 255");
}
final int r = rgb[0];
final int g = rgb[1];
final int b = rgb[2];
int max = (r > g) ? r : g;
if (b > max) {
max = b;
}
int min = (r < g) ? r : g;
if (b < min) {
min = b;
}
float tmpHue;
final float tmpBrightness = max / 2.55f;
final float tmpSaturation = (max != 0 ? ((float) (max - min)) / ((float) max) : 0) * 100.0f;
// smallest possible saturation: 0 (max=0 or max-min=0), other value closest to 0 is 100/255 (max=255, min=254)
// -> avoid float comparision to 0
// if (tmpSaturation == 0) {
if (max == 0 || (max - min) == 0) {
tmpHue = 0;
} else {
float red = ((float) (max - r)) / ((float) (max - min));
float green = ((float) (max - g)) / ((float) (max - min));
float blue = ((float) (max - b)) / ((float) (max - min));
if (r == max) {
tmpHue = blue - green;
} else if (g == max) {
tmpHue = 2.0f + red - blue;
} else {
tmpHue = 4.0f + green - red;
}
tmpHue = tmpHue / 6.0f * 360;
if (tmpHue < 0) {
tmpHue = tmpHue + 360.0f;
}
}
// adding 0.5 and casting to int approximates rounding
return new HSBType(new DecimalType((int) (tmpHue + .5f)), new PercentType((int) (tmpSaturation + .5f)),
new PercentType((int) (tmpBrightness + .5f)));
}
/**
* Transform <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space">CIE 1931</a> `xy` format to
* <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a> based {@link HSBType}.
* <a href="https://en.wikipedia.org/wiki/HSL_and_HSV">HSV</a> based {@link HSBType}.
*
* See <a href=
* "https://developers.meethue.com/develop/application-design-guidance/color-conversion-formulas-rgb-to-xy-and-back/">Hue
* developer portal</a>.
*
* @param xy the CIE 1931 xy colour, x,y[,Y] between 0.0000 and 1.0000. <code>Y</code> value is optional.
* @param xy the CIE 1931 xy color, x,y between 0.0000 and 1.0000.
* @return the corresponding {@link HSBType}.
* @throws IllegalArgumentException when input array has wrong size or exceeds allowed value range
*/
public static HSBType xyToHsb(double[] xy) throws IllegalArgumentException {
return xyToHsb(xy, DEFAULT_GAMUT);
}
/**
* Transform <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space">CIE 1931</a> `xy` format to
* <a href="https://en.wikipedia.org/wiki/HSL_and_HSV">HSV</a> based {@link HSBType}.
*
* See <a href=
* "https://developers.meethue.com/develop/application-design-guidance/color-conversion-formulas-rgb-to-xy-and-back/">Hue
* developer portal</a>.
*
* @param xy the CIE 1931 xy color, x,y[,Y] between 0.0000 and 1.0000. <code>Y</code> value is optional.
* @param gamut the gamut supported by the light.
* @return the corresponding {@link HSBType}.
* @throws IllegalArgumentException when input array has wrong size or exceeds allowed value range
*/
public static HSBType xyToHsv(double[] xy, Gamut gamut) {
public static HSBType xyToHsb(double[] xy, Gamut gamut) throws IllegalArgumentException {
if (xy.length < 2 || xy.length > 3 || !inRange(xy[0]) || !inRange(xy[1])
|| (xy.length == 3 && !inRange(xy[2]))) {
throw new IllegalArgumentException("xy array only allowes two or three values between 0.0 and 1.0.");
@ -156,8 +315,8 @@ public class ColorUtil {
b /= max;
}
HSBType hsb = HSBType.fromRGB((int) Math.round(255.0 * r), (int) Math.round(255.0 * g),
(int) Math.round(255.0 * b));
HSBType hsb = rgbToHsb(
new int[] { (int) Math.round(255.0 * r), (int) Math.round(255.0 * g), (int) Math.round(255.0 * b) });
LOGGER.trace("xy: {} - XYZ: {} {} {} - RGB: {} {} {} - HSB: {} ", xy, X, Y, Z, r, g, b, hsb);
return hsb;
@ -295,7 +454,16 @@ public class ColorUtil {
}
}
private static boolean inByteRange(int val) {
return val >= 0 && val <= 255;
}
private static boolean inRange(double val) {
return val >= 0.0 && val <= 1.0;
}
private static int convertColorPercentToByte(PercentType percent) {
return percent.toBigDecimal().multiply(BigDecimal.valueOf(255))
.divide(BIG_DECIMAL_HUNDRED, 0, RoundingMode.HALF_UP).intValue();
}
}

View File

@ -46,7 +46,7 @@ public class HSBTypeTest {
HSBType hsb = new HSBType("316,69,47");
assertEquals("color 316,69,47", hsb.format("color %hsb%"));
assertEquals("color 119,37,97", hsb.format("color %rgb%"));
assertEquals("color 120,37,98", hsb.format("color %rgb%"));
assertEquals("color 316,69,47", hsb.format("color %s"));
}
@ -85,8 +85,8 @@ public class HSBTypeTest {
compareRgbToHsbValues("240,100,100", 0, 0, 255); // blue
compareRgbToHsbValues("60,60,60", 153, 153, 61); // green
compareRgbToHsbValues("300,100,40", 102, 0, 102);
compareRgbToHsbValues("228,37,61", 99, 110, 158); // blueish
compareRgbToHsbValues("316,68,46", 119, 37, 97); // purple
compareRgbToHsbValues("229,37,62", 99, 110, 158); // blueish
compareRgbToHsbValues("316,69,47", 119, 37, 97); // purple
}
private void compareRgbToHsbValues(String hsbValues, int red, int green, int blue) {
@ -198,4 +198,19 @@ public class HSBTypeTest {
public void testConstructorWithIllegalBrightnessValue() {
assertThrows(IllegalArgumentException.class, () -> new HSBType("5,85,151"));
}
@Test
public void testCloseTo() {
HSBType hsb1 = new HSBType("5,85,11");
HSBType hsb2 = new HSBType("4,84,12");
HSBType hsb3 = new HSBType("1,8,99");
assertThrows(IllegalArgumentException.class, () -> hsb1.closeTo(hsb2, 0.0));
assertThrows(IllegalArgumentException.class, () -> hsb1.closeTo(hsb2, 1.1));
assertDoesNotThrow(() -> hsb1.closeTo(hsb2, 0.1));
assertTrue(hsb1.closeTo(hsb2, 0.01));
assertTrue(!hsb1.closeTo(hsb3, 0.01));
assertTrue(hsb1.closeTo(hsb3, 0.5));
}
}

View File

@ -16,42 +16,40 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.*;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType;
/**
* The {@link ColorUtilTest} is a test class for the color conversion
*
* @author Jan N. Klug - Initial contribution
* @author Holger Friedrich - Parameterized tests for RGB and HSB conversion
*/
@NonNullByDefault
public class ColorUtilTest {
private static Stream<Arguments> colors() {
return Stream.of(HSBType.BLACK, HSBType.BLUE, HSBType.GREEN, HSBType.RED, HSBType.WHITE,
HSBType.fromRGB(127, 94, 19)).map(Arguments::of);
}
private static Stream<Arguments> invalids() {
return Stream.of(new double[] { 0.0 }, new double[] { -1.0, 0.5 }, new double[] { 1.5, 0.5 },
new double[] { 0.5, -1.0 }, new double[] { 0.5, 1.5 }, new double[] { 0.5, 0.5, -1.0 },
new double[] { 0.5, 0.5, 1.5 }, new double[] { 0.0, 1.0, 0.0, 1.0 }).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("colors")
public void inversionTest(HSBType hsb) {
HSBType hsb2 = ColorUtil.xyToHsv(ColorUtil.hsbToXY(hsb));
HSBType hsb2 = ColorUtil.xyToHsb(ColorUtil.hsbToXY(hsb));
double deltaHue = Math.abs(hsb.getHue().doubleValue() - hsb2.getHue().doubleValue());
deltaHue = deltaHue > 180.0 ? Math.abs(deltaHue - 360) : deltaHue; // if deltaHue > 180, the "other direction"
// is shorter
// if deltaHue > 180, the "other direction" is shorter
deltaHue = deltaHue > 180.0 ? Math.abs(deltaHue - 360) : deltaHue;
double deltaSat = Math.abs(hsb.getSaturation().doubleValue() - hsb2.getSaturation().doubleValue());
double deltaBri = Math.abs(hsb.getBrightness().doubleValue() - hsb2.getBrightness().doubleValue());
@ -63,6 +61,146 @@ public class ColorUtilTest {
@ParameterizedTest
@MethodSource("invalids")
public void invalidXyValues(double[] xy) {
assertThrows(IllegalArgumentException.class, () -> ColorUtil.xyToHsv(xy));
assertThrows(IllegalArgumentException.class, () -> ColorUtil.xyToHsb(xy));
}
@Test
public void testConversionToXY() {
HSBType hsb = new HSBType("220,90,50");
PercentType[] xy = hsb.toXY();
assertEquals(14.65, xy[0].doubleValue(), 0.01);
assertEquals(11.56, xy[1].doubleValue(), 0.01);
}
// test RGB -> HSB -> RGB conversion for different values, including the ones known to cause rounding error
@ParameterizedTest
@ArgumentsSource(RgbValueProvider.class)
public void testConversionRgbToHsbToRgb(int[] rgb, int maxSquaredSum) {
HSBType hsb = ColorUtil.rgbToHsb(rgb);
Assertions.assertNotNull(hsb);
final int[] convertedRgb = ColorUtil.hsbToRgb(hsb);
assertRgbEquals(rgb, convertedRgb, maxSquaredSum);
}
@ParameterizedTest
@ArgumentsSource(HsbRgbProvider.class)
public void testConversionHsbToRgb(int[] hsb, int[] rgb) {
final String hsbString = hsb[0] + ", " + hsb[1] + ", " + hsb[2];
final HSBType hsbType = new HSBType(hsbString);
final int[] converted = ColorUtil.hsbToRgb(hsbType);
assertRgbEquals(rgb, converted, 0);
}
@ParameterizedTest
@ArgumentsSource(HsbRgbProvider.class)
public void testConversionRgbToRgb(int[] hsb, int[] rgb) {
final HSBType hsbType = ColorUtil.rgbToHsb(rgb);
final int[] rgbConverted = ColorUtil.hsbToRgb(hsbType);
assertRgbEquals(rgb, rgbConverted, 0);
}
@ParameterizedTest
@ArgumentsSource(HsbRgbProvider.class)
public void testConversionRgbToHsb(int[] hsb, int[] rgb) {
HSBType hsbType = ColorUtil.rgbToHsb(rgb);
final String expected = hsb[0] + ", " + hsb[1] + ", " + hsb[2];
// compare in HSB space, threshold 1% difference
assertTrue(hsbType.closeTo(new HSBType(expected), 0.01));
}
/* Providers for parameterized tests */
private static Stream<Arguments> colors() {
return Stream.of(HSBType.BLACK, HSBType.BLUE, HSBType.GREEN, HSBType.RED, HSBType.WHITE,
ColorUtil.rgbToHsb(new int[] { 127, 94, 19 })).map(Arguments::of);
}
private static Stream<Arguments> invalids() {
return Stream.of(new double[] { 0.0 }, new double[] { -1.0, 0.5 }, new double[] { 1.5, 0.5 },
new double[] { 0.5, -1.0 }, new double[] { 0.5, 1.5 }, new double[] { 0.5, 0.5, -1.0 },
new double[] { 0.5, 0.5, 1.5 }, new double[] { 0.0, 1.0, 0.0, 1.0 }).map(Arguments::of);
}
/*
* return a stream of well known HSB - RGB pairs
*/
static class HsbRgbProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(@Nullable ExtensionContext context) throws Exception {
return Stream.of(Arguments.of(new int[] { 0, 0, 0 }, new int[] { 0, 0, 0 }),
Arguments.of(new int[] { 0, 0, 100 }, new int[] { 255, 255, 255 }),
Arguments.of(new int[] { 0, 100, 100 }, new int[] { 255, 0, 0 }),
Arguments.of(new int[] { 120, 100, 100 }, new int[] { 0, 255, 0 }),
Arguments.of(new int[] { 240, 100, 100 }, new int[] { 0, 0, 255 }),
Arguments.of(new int[] { 60, 100, 100 }, new int[] { 255, 255, 0 }),
Arguments.of(new int[] { 180, 100, 100 }, new int[] { 0, 255, 255 }),
Arguments.of(new int[] { 300, 100, 100 }, new int[] { 255, 0, 255 }),
Arguments.of(new int[] { 0, 0, 75 }, new int[] { 191, 191, 191 }),
Arguments.of(new int[] { 0, 0, 50 }, new int[] { 128, 128, 128 }),
Arguments.of(new int[] { 0, 100, 50 }, new int[] { 128, 0, 0 }),
Arguments.of(new int[] { 60, 100, 50 }, new int[] { 128, 128, 0 }),
Arguments.of(new int[] { 120, 100, 50 }, new int[] { 0, 128, 0 }),
Arguments.of(new int[] { 300, 100, 50 }, new int[] { 128, 0, 128 }),
Arguments.of(new int[] { 180, 100, 50 }, new int[] { 0, 128, 128 }),
Arguments.of(new int[] { 240, 100, 50 }, new int[] { 0, 0, 128 }));
}
}
/*
* Return a stream RGB values together with allowed deviation (sum of squared differences).
* Differences in conversion are due to rounding errors as HSBType is created with integer numbers.
*/
static class RgbValueProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(@Nullable ExtensionContext context) throws Exception {
return Stream.of(Arguments.of(new int[] { 0, 0, 0 }, 0), Arguments.of(new int[] { 255, 255, 255 }, 0),
Arguments.of(new int[] { 255, 0, 0 }, 0), Arguments.of(new int[] { 0, 255, 0 }, 0),
Arguments.of(new int[] { 0, 0, 255 }, 0), Arguments.of(new int[] { 255, 255, 0 }, 0),
Arguments.of(new int[] { 255, 0, 255 }, 0), Arguments.of(new int[] { 0, 255, 255 }, 0),
Arguments.of(new int[] { 191, 191, 191 }, 0), Arguments.of(new int[] { 128, 128, 128 }, 0),
Arguments.of(new int[] { 128, 0, 0 }, 0), Arguments.of(new int[] { 128, 128, 0 }, 0),
Arguments.of(new int[] { 0, 128, 0 }, 0), Arguments.of(new int[] { 128, 0, 128 }, 0),
Arguments.of(new int[] { 0, 128, 128 }, 0), Arguments.of(new int[] { 0, 0, 128 }, 0),
Arguments.of(new int[] { 0, 132, 255 }, 0), Arguments.of(new int[] { 1, 131, 254 }, 3),
Arguments.of(new int[] { 2, 130, 253 }, 6), Arguments.of(new int[] { 3, 129, 252 }, 4),
Arguments.of(new int[] { 4, 128, 251 }, 3), Arguments.of(new int[] { 5, 127, 250 }, 0));
}
}
/* Helper functions */
/**
* Helper method for checking if expected and actual RGB color parameters (int[3], 0..255) lie within a given
* percentage of each other. This method is required in order to eliminate integer rounding artifacts in JUnit tests
* when comparing RGB values. Asserts that the color parameters of expected and actual do not have a squared sum
* of differences which exceeds maxSquaredSum.
*
* When the test fails, both colors are printed.
*
* @param expected an HSBType containing the expected color.
* @param actual an HSBType containing the actual color.
* @param maxSquaredSum the maximum allowed squared sum of differences.
*/
private void assertRgbEquals(final int[] expected, final int[] actual, int maxSquaredSum) {
int squaredSum = 0;
if (expected[0] != actual[0] || expected[1] != actual[1] || expected[2] != actual[2]) {
// only proceed if both RGB colors are not idential
for (int i = 0; i < 3; i++) {
int diff = expected[i] - actual[i];
squaredSum = squaredSum + diff * diff;
}
if (squaredSum > maxSquaredSum) {
// deviation too high, just prepare readable string compare and let it fail
final String expectedS = expected[0] + ", " + expected[1] + ", " + expected[2];
final String actualS = actual[0] + ", " + actual[1] + ", " + actual[2];
assertEquals(expectedS, actualS);
}
}
}
}