[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-mesh-generators"))
implementation(project(":orx-color"))
implementation(project(":orx-jvm:orx-gui"))
implementation("org.openrndr:openrndr-application:$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 h = 0.5 + 0.5 * atan2(-lab.b, -lab.a) / PI;
val Cs = get_Cs(L, a_, b_)
val C_0 = Cs[0];
val C_mid = Cs[1];
val C_max = Cs[2];
val cs = get_Cs(L, a_, b_)
val c0 = cs[0];
val cMid = cs[1];
val cMax = cs[2];
val s = if (C < C_mid) {
val k_0 = 0;
val k_1 = 0.8 * C_0;
val k_2 = (1 - k_1 / C_mid);
val s = if (C < cMid) {
val k0 = 0;
val k1 = 0.8 * c0;
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;
} else {
val k_0 = C_mid;
val k_1 = 0.2 * C_mid * C_mid * 1.25 * 1.25 / C_0;
val k_2 = (1 - (k_1) / (C_max - C_mid));
val k0 = cMid;
val k1 = 0.2 * cMid * cMid * 1.25 * 1.25 / c0;
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;
}
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) {
ColorRGBa(0.0, 0.0, 0.0, a)
}
val a_ = cos(2 * PI * h);
val b_ = sin(2 * PI * h);
val L = toe_inv(l);
val a_ = cos(2 * PI * h / 360.0);
val b_ = sin(2 * PI * h / 360.0);
val L = toeInv(l);
val Cs = get_Cs(L, a_, b_);
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[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 {
val normalizedShift = shiftInDegrees / 360.0
return copy(h = h + normalizedShift)
return copy(h = h + shiftInDegrees)
}
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 {
val sx = factor.coerceIn(0.0, 1.0)
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) * l + sx * other.l,
(1.0 - sx) * a + sx * other.a

View File

@@ -4,7 +4,6 @@ import org.openrndr.color.*
import org.openrndr.math.mixAngle
import kotlin.math.*
data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Double = 1.0) :
HueShiftableColor<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 {
val lab = c.toOKLABa()
var C = sqrt(lab.a * lab.a + lab.b * lab.b);
val a_ = lab.a / C;
val b_ = lab.b / C;
val a_ = if (C != 0.0) lab.a / C else 0.0
val b_ = if (C != 0.0) lab.b / C else 0.0
var L = lab.l
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_0 = 0.5;
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 L_v = t * L;
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 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 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 {
val a_ = cos(2 * PI * h)
val b_ = sin(2 * PI * h)
val a_ = cos(2 * PI * h / 360.0)
val b_ = sin(2 * PI * h / 360.0)
val ST_max = get_ST_max(a_, b_)
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));
//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 L_new = toe_inv(L); // * L_v/L_vt;
val L_new = toeInv(L); // * L_v/L_vt;
C = C * L_new / L;
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;
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 {
val normalizedShift = shiftInDegrees / 360.0
return copy(h = h + normalizedShift)
return copy(h = h + shiftInDegrees)
}
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 {
val sx = factor.coerceIn(0.0, 1.0)
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) * v + sx * other.v,
(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))
}
fun toe(x: Double): Double {
val k_1 = 0.206
val k_2 = 0.03
val k_3 = (1 + k_1) / (1 + k_2)
val k1 = 0.206
val k2 = 0.03
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 {
val k_1 = 0.206
val k_2 = 0.03
val k_3 = (1 + k_1) / (1 + k_2)
return (x * x + k_1 * x) / (k_3 * (x + k_2))
fun toeInv(x: Double): Double {
val k1 = 0.206
val k2 = 0.03
val k3 = (1 + k1) / (1 + k2)
return (x * x + k1 * x) / (k3 * (x + k2))
}
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)
}
}
}