[orx-color] Fix NaN bugs in ColorOKHSLa, ColorOKHSVa

This commit is contained in:
Edwin Jakobs
2021-11-22 18:17:42 +01:00
parent 7ff626fa8a
commit c9201a3920
6 changed files with 139 additions and 45 deletions

View File

@@ -28,6 +28,7 @@ kotlin {
implementation(project(":orx-camera")) implementation(project(":orx-camera"))
implementation(project(":orx-mesh-generators")) implementation(project(":orx-mesh-generators"))
implementation(project(":orx-color")) implementation(project(":orx-color"))
implementation(project(":orx-jvm:orx-gui"))
implementation("org.openrndr:openrndr-application:$openrndrVersion") implementation("org.openrndr:openrndr-application:$openrndrVersion")
implementation("org.openrndr:openrndr-extensions:$openrndrVersion") implementation("org.openrndr:openrndr-extensions:$openrndrVersion")

View File

@@ -22,29 +22,29 @@ data class ColorOKHSLa(val h: Double, val s: Double, val l: Double, val a: Doubl
val L = lab.l val L = lab.l
val h = 0.5 + 0.5 * atan2(-lab.b, -lab.a) / PI; val h = 0.5 + 0.5 * atan2(-lab.b, -lab.a) / PI;
val Cs = get_Cs(L, a_, b_) val cs = get_Cs(L, a_, b_)
val C_0 = Cs[0]; val c0 = cs[0];
val C_mid = Cs[1]; val cMid = cs[1];
val C_max = Cs[2]; val cMax = cs[2];
val s = if (C < C_mid) { val s = if (C < cMid) {
val k_0 = 0; val k0 = 0;
val k_1 = 0.8 * C_0; val k1 = 0.8 * c0;
val k_2 = (1 - k_1 / C_mid); val k2 = (1 - k1 / cMid);
val t = (C - k_0) / (k_1 + k_2 * (C - k_0)); val t = (C - k0) / (k1 + k2 * (C - k0));
t * 0.8; t * 0.8;
} else { } else {
val k_0 = C_mid; val k0 = cMid;
val k_1 = 0.2 * C_mid * C_mid * 1.25 * 1.25 / C_0; val k1 = 0.2 * cMid * cMid * 1.25 * 1.25 / c0;
val k_2 = (1 - (k_1) / (C_max - C_mid)); val k2 = (1 - (k1) / (cMax - cMid));
val t = (C - k_0) / (k_1 + k_2 * (C - k_0)); val t = (C - k0) / (k1 + k2 * (C - k0));
0.8 + 0.2 * t; 0.8 + 0.2 * t;
} }
val l = toe(L); val l = toe(L);
return ColorOKHSLa(h, s, l, c.a) return ColorOKHSLa(h * 360.0, if (s == s) s else 0.0, if (l == l) l else 0.0, c.a)
} }
} }
@@ -54,9 +54,9 @@ data class ColorOKHSLa(val h: Double, val s: Double, val l: Double, val a: Doubl
} else if (l == 0.0) { } else if (l == 0.0) {
ColorRGBa(0.0, 0.0, 0.0, a) ColorRGBa(0.0, 0.0, 0.0, a)
} }
val a_ = cos(2 * PI * h); val a_ = cos(2 * PI * h / 360.0);
val b_ = sin(2 * PI * h); val b_ = sin(2 * PI * h / 360.0);
val L = toe_inv(l); val L = toeInv(l);
val Cs = get_Cs(L, a_, b_); val Cs = get_Cs(L, a_, b_);
val C_0 = Cs[0]; val C_0 = Cs[0];
@@ -94,12 +94,11 @@ data class ColorOKHSLa(val h: Double, val s: Double, val l: Double, val a: Doubl
// 255*srgb_transfer_function(rgb[1]), // 255*srgb_transfer_function(rgb[1]),
// 255*srgb_transfer_function(rgb[2]), // 255*srgb_transfer_function(rgb[2]),
// ] // ]
return ColorOKLABa(L, C * a_, C * b_).toRGBa().toSRGB() return ColorOKLABa(if (L == L) L else 0.0, if (C == C) C * a_ else 0.0, if (C == C) C * b_ else 0.0).toRGBa().toSRGB()
} }
override fun shiftHue(shiftInDegrees: Double): ColorOKHSLa { override fun shiftHue(shiftInDegrees: Double): ColorOKHSLa {
val normalizedShift = shiftInDegrees / 360.0 return copy(h = h + shiftInDegrees)
return copy(h = h + normalizedShift)
} }
override fun opacify(factor: Double): ColorOKHSLa { override fun opacify(factor: Double): ColorOKHSLa {
@@ -125,7 +124,7 @@ data class ColorOKHSLa(val h: Double, val s: Double, val l: Double, val a: Doubl
override fun mix(other: ColorOKHSLa, factor: Double): ColorOKHSLa { override fun mix(other: ColorOKHSLa, factor: Double): ColorOKHSLa {
val sx = factor.coerceIn(0.0, 1.0) val sx = factor.coerceIn(0.0, 1.0)
return ColorOKHSLa( return ColorOKHSLa(
mixAngle(h * 360.0, other.h * 360.0, sx) / 360.0, mixAngle(h, other.h, sx) / 360.0,
(1.0 - sx) * s + sx * other.s, (1.0 - sx) * s + sx * other.s,
(1.0 - sx) * l + sx * other.l, (1.0 - sx) * l + sx * other.l,
(1.0 - sx) * a + sx * other.a (1.0 - sx) * a + sx * other.a

View File

@@ -4,7 +4,6 @@ import org.openrndr.color.*
import org.openrndr.math.mixAngle import org.openrndr.math.mixAngle
import kotlin.math.* import kotlin.math.*
data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Double = 1.0) : data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Double = 1.0) :
HueShiftableColor<ColorOKHSVa>, HueShiftableColor<ColorOKHSVa>,
OpacifiableColor<ColorOKHSVa>, OpacifiableColor<ColorOKHSVa>,
@@ -17,8 +16,8 @@ data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Doubl
fun fromColorRGBa(c: ColorRGBa): ColorOKHSVa { fun fromColorRGBa(c: ColorRGBa): ColorOKHSVa {
val lab = c.toOKLABa() val lab = c.toOKLABa()
var C = sqrt(lab.a * lab.a + lab.b * lab.b); var C = sqrt(lab.a * lab.a + lab.b * lab.b);
val a_ = lab.a / C; val a_ = if (C != 0.0) lab.a / C else 0.0
val b_ = lab.b / C; val b_ = if (C != 0.0) lab.b / C else 0.0
var L = lab.l var L = lab.l
val h = 0.5 + 0.5 * atan2(-lab.b, -lab.a) / PI; val h = 0.5 + 0.5 * atan2(-lab.b, -lab.a) / PI;
@@ -27,13 +26,13 @@ data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Doubl
val S_max = ST_max[0]; val S_max = ST_max[0];
val S_0 = 0.5; val S_0 = 0.5;
val T = ST_max[1]; val T = ST_max[1];
val k = 1 - S_0 / S_max; val k = if (S_max != 0.0) (1 - S_0 / S_max) else 0.0
val t = T / (C + L * T); val t = T / (C + L * T);
val L_v = t * L; val L_v = t * L;
val C_v = t * C; val C_v = t * C;
val L_vt = toe_inv(L_v); val L_vt = toeInv(L_v);
val C_vt = C_v * L_vt / L_v; val C_vt = C_v * L_vt / L_v;
val rgb_scale = ColorOKLABa(L_vt, a_ * C_vt, b_ * C_vt, c.a).toRGBa().toLinear() val rgb_scale = ColorOKLABa(L_vt, a_ * C_vt, b_ * C_vt, c.a).toRGBa().toLinear()
@@ -48,13 +47,13 @@ data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Doubl
val v = L / L_v; val v = L / L_v;
val s = (S_0 + T) * C_v / ((T * S_0) + T * k * C_v) val s = (S_0 + T) * C_v / ((T * S_0) + T * k * C_v)
return ColorOKHSVa(h, s, v, c.a) return ColorOKHSVa(h * 360.0, if (s == s) s else 0.0, if (v==v) v else 0.0, c.a)
} }
} }
override fun toRGBa(): ColorRGBa { override fun toRGBa(): ColorRGBa {
val a_ = cos(2 * PI * h) val a_ = cos(2 * PI * h / 360.0)
val b_ = sin(2 * PI * h) val b_ = sin(2 * PI * h / 360.0)
val ST_max = get_ST_max(a_, b_) val ST_max = get_ST_max(a_, b_)
val S_max = ST_max[0]; val S_max = ST_max[0];
@@ -74,10 +73,10 @@ data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Doubl
//L = v*(1 - s*S_max/(S_max+T)); //L = v*(1 - s*S_max/(S_max+T));
//C = v*s*S_max*T/(S_max+T); //C = v*s*S_max*T/(S_max+T);
val L_vt = toe_inv(L_v); val L_vt = toeInv(L_v);
val C_vt = C_v * L_vt / L_v; val C_vt = C_v * L_vt / L_v;
val L_new = toe_inv(L); // * L_v/L_vt; val L_new = toeInv(L); // * L_v/L_vt;
C = C * L_new / L; C = C * L_new / L;
L = L_new; L = L_new;
@@ -89,12 +88,14 @@ data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Doubl
L *= scale_L; L *= scale_L;
C *= scale_L; C *= scale_L;
return ColorOKLABa(L, C * a_, C * b_).toRGBa().toSRGB() return ColorOKLABa(
if (L == L) L else 0.0,
if (C == C) C * a_ else 0.0,
if (C == C) C * b_ else 0.0).toRGBa().toSRGB()
} }
override fun shiftHue(shiftInDegrees: Double): ColorOKHSVa { override fun shiftHue(shiftInDegrees: Double): ColorOKHSVa {
val normalizedShift = shiftInDegrees / 360.0 return copy(h = h + shiftInDegrees)
return copy(h = h + normalizedShift)
} }
override fun opacify(factor: Double): ColorOKHSVa { override fun opacify(factor: Double): ColorOKHSVa {
@@ -120,7 +121,7 @@ data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Doubl
override fun mix(other: ColorOKHSVa, factor: Double): ColorOKHSVa { override fun mix(other: ColorOKHSVa, factor: Double): ColorOKHSVa {
val sx = factor.coerceIn(0.0, 1.0) val sx = factor.coerceIn(0.0, 1.0)
return ColorOKHSVa( return ColorOKHSVa(
mixAngle(h * 360.0, other.h * 360.0, sx) / 360.0, mixAngle(h, other.h, sx),
(1.0 - sx) * s + sx * other.s, (1.0 - sx) * s + sx * other.s,
(1.0 - sx) * v + sx * other.v, (1.0 - sx) * v + sx * other.v,
(1.0 - sx) * a + sx * other.a (1.0 - sx) * a + sx * other.a

View File

@@ -12,20 +12,21 @@ internal fun max(a: Double, b: Double, c: Double, d: Double): Double {
return max(max(a, b), max(c, d)) return max(max(a, b), max(c, d))
} }
fun toe(x: Double): Double { fun toe(x: Double): Double {
val k_1 = 0.206 val k1 = 0.206
val k_2 = 0.03 val k2 = 0.03
val k_3 = (1 + k_1) / (1 + k_2) val k3 = (1 + k1) / (1 + k2)
return 0.5 * (k_3 * x - k_1 + sqrt((k_3 * x - k_1) * (k_3 * x - k_1) + 4 * k_2 * k_3 * x)) val d = (k3 * x - k1) * (k3 * x - k1) + 4 * k2 * k3 * x
return 0.5 * (k3 * x - k1 + sqrt(d.coerceAtLeast(0.0)))
} }
fun toe_inv(x: Double): Double { fun toeInv(x: Double): Double {
val k_1 = 0.206 val k1 = 0.206
val k_2 = 0.03 val k2 = 0.03
val k_3 = (1 + k_1) / (1 + k_2) val k3 = (1 + k1) / (1 + k2)
return (x * x + k_1 * x) / (k_3 * (x + k_2)) return (x * x + k1 * x) / (k3 * (x + k2))
} }
internal fun compute_max_saturation(a: Double, b: Double): Double { internal fun compute_max_saturation(a: Double, b: Double): Double {

View File

@@ -0,0 +1,43 @@
package spaces
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.color.spaces.toOKHSLa
import org.openrndr.extra.color.spaces.toOKHSVa
import kotlin.test.Test
import kotlin.test.assertTrue
class TestOKHSLa {
@Test
fun testConversions() {
val testColors = listOf(ColorRGBa.RED, ColorRGBa.BLUE, ColorRGBa.GREEN, ColorRGBa.GRAY, ColorRGBa.YELLOW)
val error = (-1E-5 .. 1E-5)
testColors.forEach {
val testColor = it
val toColor = it.toOKHSLa()
val restoreColor = toColor.toRGBa()
assertTrue("color $testColor, $toColor, $restoreColor") {
testColor.r - restoreColor.r in error && testColor.g - restoreColor.g in error && testColor.b - restoreColor.b in error
}
}
}
@Test
fun testSaturationPersistence() {
val black = ColorRGBa.BLACK.toOKHSLa()
assertTrue("resulting OKHSLa $black contains no NaNs") {
black.h == black.h && black.s == black.s && black.l == black.l
}
val rgbBlack = black.toRGBa()
val white = ColorRGBa.WHITE.toOKHSLa()
val rgbWhite = white.toRGBa()
val epsilon = 1E-6
assertTrue("resulting color $rgbWhite is white") {
rgbWhite.r in (1.0 - epsilon .. 1.0 + epsilon) && rgbWhite.g in (1.0 - epsilon .. 1.0 + epsilon) && rgbWhite.b in (1.0 - epsilon .. 1.0 + epsilon)
}
assertTrue("resulting color $rgbBlack is black") {
rgbBlack.r in (0.0 - epsilon .. 0.0 + epsilon) && rgbBlack.g in (0.0 - epsilon .. 0.0 + epsilon) && rgbBlack.b in (0.0 - epsilon .. 0.0 + epsilon)
}
}
}

View File

@@ -0,0 +1,49 @@
package spaces
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.color.spaces.toOKHSLa
import org.openrndr.extra.color.spaces.toOKHSVa
import kotlin.test.Test
import kotlin.test.assertTrue
class TestOKHSVa {
@Test
fun testConversions() {
val testColors = listOf(ColorRGBa.RED, ColorRGBa.BLUE, ColorRGBa.GREEN, ColorRGBa.GRAY, ColorRGBa.YELLOW)
val error = (-1E-5 .. 1E-5)
testColors.forEach {
val testColor = it
val toColor = it.toOKHSVa()
val restoreColor = toColor.toRGBa()
assertTrue("color $testColor, $toColor, $restoreColor") {
testColor.r - restoreColor.r in error && testColor.g - restoreColor.g in error && testColor.b - restoreColor.b in error
}
}
}
@Test
fun testSaturationPersistence() {
val black = ColorRGBa.BLACK.toOKHSVa()
val rgbBlack = black.toRGBa()
assertTrue("resulting OKHSVa $black contains no NaNs") {
black.h == black.h && black.s == black.s && black.v == black.v
}
val white = ColorRGBa.WHITE.toOKHSVa()
val rgbWhite = white.toRGBa()
assertTrue("resulting OKHSVa $white contains no NaNs") {
white.h == white.h && white.s == white.s && white.v == white.v
}
val epsilon = 1E-6
assertTrue("resulting color is white") {
rgbWhite.r in (1.0 - epsilon .. 1.0 + epsilon) && rgbWhite.g in (1.0 - epsilon .. 1.0 + epsilon) && rgbWhite.b in (1.0 - epsilon .. 1.0 + epsilon)
}
assertTrue("resulting color is black") {
rgbBlack.r in (0.0 - epsilon .. 0.0 + epsilon) && rgbBlack.g in (0.0 - epsilon .. 0.0 + epsilon) && rgbBlack.b in (0.0 - epsilon .. 0.0 + epsilon)
}
}
}