mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-01-25 11:45:49 +01:00
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:
parent
3047ed42a5
commit
b0b8bb547b
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user