From 7f4256fa7fefae826d0c11bdd3e3622ec3b85ac6 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Fri, 18 Oct 2024 07:42:25 -0700 Subject: [PATCH 01/26] Add perceptually similar tolerance using Delta E 2000 --- .gitignore | 2 + .../kotlin/com/quickbird/snapshot/Color.kt | 73 ++++++++++++++++++- .../com/quickbird/snapshot/Diffing+bitmap.kt | 11 +-- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index c8c70f2..4ca26e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /build /.gradle +.idea +local.properties diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index cc5e2dd..9991450 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -1,8 +1,79 @@ package com.quickbird.snapshot import androidx.annotation.ColorInt +import kotlin.math.cbrt +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.sqrt -data class Color(@ColorInt val value: Int) +data class Color(@ColorInt val value: Int) { + fun equals(other: Color, any: Any) { + + } +} val @receiver:ColorInt Int.color get() = Color(this) + +fun Color.isSimilar(other: Color, perceptualTolerance: Double): Boolean { + if (this == other) { + return true + } + // Compute the Delta E 2000 difference between the two colors in the CIELAB color space and return whether it's within the perceptual tolerance + // + return min(this.difference(other) / 100, 1.0) <= perceptualTolerance +} + +// Convert the color to the CIELAB color space +// +private fun Color.toLAB(): DoubleArray { + val r = (value shr 16 and 0xff) / 255.0 + val g = (value shr 8 and 0xff) / 255.0 + val b = (value and 0xff) / 255.0 + + val x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375 + val y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750 + val z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041 + + val l1 = 116 * f(y / 1.0) - 16 + val a1 = 500 * (f(x / 0.95047) - f(y / 1.0)) + val b1 = 200 * (f(y / 1.0) - f(z / 1.08883)) + + return doubleArrayOf(l1, a1, b1) +} + +private fun f(t: Double): Double { + return if (t > 0.008856) cbrt(t) else (7.787 * t) + (16 / 116.0) +} + +// Calculates CIEDE2000 (Delta E 2000) between two colors in the CIELAB color space returning a value between 0-100 (0 means no difference, 100 means completely opposite) +// +// This is the most recent and accurate formula, which includes corrections for perceptual uniformity +// Platform difference: iOS uses CIE94 (Delta E 1994) for color difference calculations +// +private fun Color.difference(other: Color): Double { + val lab1 = this.toLAB() + val lab2 = other.toLAB() + + val deltaL = lab1[0] - lab2[0] + val lBar = (lab1[0] + lab2[0]) / 2.0 + val c1 = sqrt(lab1[1].pow(2) + lab1[2].pow(2)) + val c2 = sqrt(lab2[1].pow(2) + lab2[2].pow(2)) + val cBar = (c1 + c2) / 2.0 + val deltaC = c1 - c2 + val deltaA = lab1[1] - lab2[1] + val deltaB = lab1[2] - lab2[2] + val deltaH = sqrt(deltaA.pow(2) + deltaB.pow(2) - deltaC.pow(2)) + + val sl = 1.0 + val kc = 1.0 + val kh = 1.0 + val sc = 1.0 + 0.045 * cBar + val sh = 1.0 + 0.015 * cBar + + return sqrt( + (deltaL / sl).pow(2) + + (deltaC / (kc * sc)).pow(2) + + (deltaH / (kh * sh)).pow(2) + ) +} \ No newline at end of file diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index a38c1b4..39b2db3 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -5,9 +5,10 @@ import android.graphics.Color as AndroidColor fun Diffing.Companion.bitmap( colorDiffing: Diffing, - tolerance: Double = 0.0 + tolerance: Double = 0.0, + perceptualTolerance: Double = 0.0 ) = Diffing { first, second -> - val difference = first differenceTo second + val difference = first.differenceTo(second, perceptualTolerance) if (difference <= tolerance) null else first.copy(first.config, true).apply { @@ -36,14 +37,14 @@ val Diffing.Companion.intMean else first / 2 + second / 2 } -private infix fun Bitmap.differenceTo(other: Bitmap): Double { +private fun Bitmap.differenceTo(other: Bitmap, perceptualTolerance: Double): Double { val thisPixels = this.pixels val otherPixels = other.pixels if (thisPixels.size != otherPixels.size) return 100.0 val differentPixelCount = thisPixels - .zip(otherPixels, Color::equals) + .zip(otherPixels) { a, b -> a.isSimilar(b, perceptualTolerance) } .count { !it } return differentPixelCount.toDouble() / thisPixels.size -} +} \ No newline at end of file From 39f50fa35e22db914e2b2f1496af6e9326bd7d5d Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Mon, 21 Oct 2024 07:38:39 -0700 Subject: [PATCH 02/26] Add logs --- .../kotlin/com/quickbird/snapshot/Color.kt | 6 ++-- .../com/quickbird/snapshot/Diffing+bitmap.kt | 31 +++++++++++++------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index 9991450..792d482 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -15,13 +15,13 @@ data class Color(@ColorInt val value: Int) { val @receiver:ColorInt Int.color get() = Color(this) -fun Color.isSimilar(other: Color, perceptualTolerance: Double): Boolean { +fun Color.deltaE(other: Color): Double { if (this == other) { - return true + return 0.0 } // Compute the Delta E 2000 difference between the two colors in the CIELAB color space and return whether it's within the perceptual tolerance // - return min(this.difference(other) / 100, 1.0) <= perceptualTolerance + return min(this.difference(other) / 100, 1.0) } // Convert the color to the CIELAB color space diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index 39b2db3..be3884f 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -1,8 +1,11 @@ package com.quickbird.snapshot import android.graphics.Bitmap +import android.util.Log import android.graphics.Color as AndroidColor +private var maximumDeltaE: Double? = null + fun Diffing.Companion.bitmap( colorDiffing: Diffing, tolerance: Double = 0.0, @@ -10,12 +13,19 @@ fun Diffing.Companion.bitmap( ) = Diffing { first, second -> val difference = first.differenceTo(second, perceptualTolerance) - if (difference <= tolerance) null - else first.copy(first.config, true).apply { - updatePixels { x, y, color -> - if (x < second.width && y < second.height) - colorDiffing(color, second.getPixel(x, y).color) ?: color - else color + if (difference <= tolerance) { + null + } else { + var log = "Actual image precision $difference is greater than required $tolerance" + if (maximumDeltaE != null) log += ", Actual perceptual precision $maximumDeltaE is greater than required $perceptualTolerance" + Log.e("Snapshot diffing", log) + + first.copy(first.config, true).apply { + updatePixels { x, y, color -> + if (x < second.width && y < second.height) + colorDiffing(color, second.getPixel(x, y).color) ?: color + else color + } } } } @@ -42,9 +52,10 @@ private fun Bitmap.differenceTo(other: Bitmap, perceptualTolerance: Double): Dou val otherPixels = other.pixels if (thisPixels.size != otherPixels.size) return 100.0 - val differentPixelCount = thisPixels - .zip(otherPixels) { a, b -> a.isSimilar(b, perceptualTolerance) } - .count { !it } + val deltaEPixels = thisPixels + .zip(otherPixels, Color::deltaE) + val deltaEDifference = deltaEPixels.count { it < perceptualTolerance } + maximumDeltaE = deltaEPixels.maxOrNull() ?: 0.0 - return differentPixelCount.toDouble() / thisPixels.size + return deltaEDifference.toDouble() / thisPixels.size } \ No newline at end of file From 9bd580deaeec6c01c25598c659812ae6fed701ab Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Mon, 21 Oct 2024 08:10:16 -0700 Subject: [PATCH 03/26] Logs --- .../kotlin/com/quickbird/snapshot/Color.kt | 11 +++++------ .../kotlin/com/quickbird/snapshot/Diffing+bitmap.kt | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index 792d482..82cb1de 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -1,16 +1,13 @@ package com.quickbird.snapshot +import android.util.Log import androidx.annotation.ColorInt import kotlin.math.cbrt import kotlin.math.min import kotlin.math.pow import kotlin.math.sqrt -data class Color(@ColorInt val value: Int) { - fun equals(other: Color, any: Any) { - - } -} +data class Color(@ColorInt val value: Int) val @receiver:ColorInt Int.color get() = Color(this) @@ -71,9 +68,11 @@ private fun Color.difference(other: Color): Double { val sc = 1.0 + 0.045 * cBar val sh = 1.0 + 0.015 * cBar - return sqrt( + val deltaE = sqrt( (deltaL / sl).pow(2) + (deltaC / (kc * sc)).pow(2) + (deltaH / (kh * sh)).pow(2) ) + Log.d("SnapshotDiffing", "Delta E: $deltaE") + return deltaE } \ No newline at end of file diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index be3884f..dd1fd9f 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -18,7 +18,7 @@ fun Diffing.Companion.bitmap( } else { var log = "Actual image precision $difference is greater than required $tolerance" if (maximumDeltaE != null) log += ", Actual perceptual precision $maximumDeltaE is greater than required $perceptualTolerance" - Log.e("Snapshot diffing", log) + Log.e("SnapshotDiffing", log) first.copy(first.config, true).apply { updatePixels { x, y, color -> From a142bc169074dc223e0229a4633ea6234b7f75b5 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Mon, 21 Oct 2024 12:27:32 -0700 Subject: [PATCH 04/26] Updates --- .../kotlin/com/quickbird/snapshot/Color.kt | 64 +++++++++++++------ .../com/quickbird/snapshot/Diffing+bitmap.kt | 6 +- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index 82cb1de..f7a7b08 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -2,7 +2,10 @@ package com.quickbird.snapshot import android.util.Log import androidx.annotation.ColorInt +import kotlin.math.PI +import kotlin.math.atan2 import kotlin.math.cbrt +import kotlin.math.max import kotlin.math.min import kotlin.math.pow import kotlin.math.sqrt @@ -21,6 +24,19 @@ fun Color.deltaE(other: Color): Double { return min(this.difference(other) / 100, 1.0) } +// Convert the color to the Lch color space +// +private fun Color.toLch(): DoubleArray { + val lab = this.toLAB() + val l = lab[0] + val a = lab[1] + val b = lab[2] + + val c = sqrt(a * a + b * b) + val h = atan2(b, a) * (180 / PI) + return doubleArrayOf(l, c, if (h >= 0) h else h + 360) +} + // Convert the color to the CIELAB color space // private fun Color.toLAB(): DoubleArray { @@ -49,29 +65,35 @@ private fun f(t: Double): Double { // Platform difference: iOS uses CIE94 (Delta E 1994) for color difference calculations // private fun Color.difference(other: Color): Double { - val lab1 = this.toLAB() - val lab2 = other.toLAB() - - val deltaL = lab1[0] - lab2[0] - val lBar = (lab1[0] + lab2[0]) / 2.0 - val c1 = sqrt(lab1[1].pow(2) + lab1[2].pow(2)) - val c2 = sqrt(lab2[1].pow(2) + lab2[2].pow(2)) - val cBar = (c1 + c2) / 2.0 - val deltaC = c1 - c2 - val deltaA = lab1[1] - lab2[1] - val deltaB = lab1[2] - lab2[2] - val deltaH = sqrt(deltaA.pow(2) + deltaB.pow(2) - deltaC.pow(2)) - - val sl = 1.0 - val kc = 1.0 - val kh = 1.0 - val sc = 1.0 + 0.045 * cBar - val sh = 1.0 + 0.015 * cBar + val (L1, C1, H1) = this.toLch() + val (L2, C2, H2) = other.toLch() + + val deltaL = L2 - L1 + val meanL = (L1 + L2) / 2 + + val deltaC = C2 - C1 + val meanC = (C1 + C2) / 2 + + val deltaH = H2 - H1 + val meanH = (H1 + H2) / 2 + + val T = 1 - 0.17 * kotlin.math.cos(Math.toRadians(meanH - 30)) + + 0.24 * kotlin.math.cos(Math.toRadians(2 * meanH)) + + 0.32 * kotlin.math.cos(Math.toRadians(3 * meanH + 6)) - + 0.20 * kotlin.math.cos(Math.toRadians(4 * meanH - 63)) + + val deltaTheta = 30 * kotlin.math.exp(-((meanH - 275) / 25).pow(2.0)) + val Rc = 2 * sqrt((meanC.pow(7.0)) / (meanC.pow(7.0) + 25.0.pow(7.0))) + val Sl = 1 + (0.015 * (meanL - 50).pow(2.0)) / sqrt(20 + (meanL - 50).pow(2.0)) + val Sc = 1 + 0.045 * meanC + val Sh = 1 + 0.015 * meanC * T + val Rt = -kotlin.math.sin(Math.toRadians(2 * deltaTheta)) * Rc val deltaE = sqrt( - (deltaL / sl).pow(2) + - (deltaC / (kc * sc)).pow(2) + - (deltaH / (kh * sh)).pow(2) + (deltaL / Sl).pow(2.0) + + (deltaC / Sc).pow(2.0) + + (deltaH / Sh).pow(2.0) + + Rt * (deltaC / Sc) * (deltaH / Sh) ) Log.d("SnapshotDiffing", "Delta E: $deltaE") return deltaE diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index dd1fd9f..2e1c655 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -16,11 +16,11 @@ fun Diffing.Companion.bitmap( if (difference <= tolerance) { null } else { - var log = "Actual image precision $difference is greater than required $tolerance" - if (maximumDeltaE != null) log += ", Actual perceptual precision $maximumDeltaE is greater than required $perceptualTolerance" + var log = "Actual image tolerance $difference is greater than required $tolerance" + if (maximumDeltaE != null) log += ", Actual perceptual tolerance $maximumDeltaE is greater than required $perceptualTolerance" Log.e("SnapshotDiffing", log) - first.copy(first.config, true).apply { + first.copy(first.config!!, true).apply { updatePixels { x, y, color -> if (x < second.width && y < second.height) colorDiffing(color, second.getPixel(x, y).color) ?: color From 1f9c0bd2c5ad15672c46702be7a26b01e76f7286 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Mon, 21 Oct 2024 15:04:17 -0700 Subject: [PATCH 05/26] Math --- .../kotlin/com/quickbird/snapshot/Color.kt | 173 +++++++++++------- .../com/quickbird/snapshot/Diffing+bitmap.kt | 9 +- 2 files changed, 114 insertions(+), 68 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index f7a7b08..a364a05 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -1,10 +1,10 @@ package com.quickbird.snapshot +import android.annotation.SuppressLint +import android.graphics.Color as AndroidColor import android.util.Log import androidx.annotation.ColorInt -import kotlin.math.PI -import kotlin.math.atan2 -import kotlin.math.cbrt +import kotlin.collections.component1 import kotlin.math.max import kotlin.math.min import kotlin.math.pow @@ -19,82 +19,127 @@ fun Color.deltaE(other: Color): Double { if (this == other) { return 0.0 } - // Compute the Delta E 2000 difference between the two colors in the CIELAB color space and return whether it's within the perceptual tolerance + // Compute the Delta E 2000 difference between the two colors in the CIE Lch color space and return whether it's within the perceptual tolerance // return min(this.difference(other) / 100, 1.0) } -// Convert the color to the Lch color space +// Convert the color to the CIE XYZ color space +// http://www.brucelindbloom.com/index.html?Calc.html // -private fun Color.toLch(): DoubleArray { - val lab = this.toLAB() - val l = lab[0] - val a = lab[1] - val b = lab[2] - - val c = sqrt(a * a + b * b) - val h = atan2(b, a) * (180 / PI) - return doubleArrayOf(l, c, if (h >= 0) h else h + 360) +@SuppressLint("NewApi") +private fun Color.toXYZ(): DoubleArray { + var r = AndroidColor.red(this.value) / 255.0 + var g = AndroidColor.green(this.value) / 255.0 + var b = AndroidColor.blue(this.value) / 255.0 + + r = if (r > 0.04045) { + ((r + 0.055) / 1.055).pow(2.4) + } else { + r / 12.92; + } + + g = if (g > 0.04045) { + ((g + 0.055) / 1.055).pow(2.4); + } else { + g / 12.92; + } + + b = if (b > 0.04045) { + ((b + 0.055) / 1.055).pow(2.4); + } else { + b / 12.92; + } + + r *= 100; + g *= 100; + b *= 100; + Log.d("SnapshotDiffing", "R: $r, G: $g, B: $b"); + + return doubleArrayOf( + (0.4124 * r + 0.3576 * g + 0.1805 * b), + (0.2126 * r + 0.7152 * g + 0.0722 * b), + (0.0193 * r + 0.1192 * g + 0.9505 * b) + ).also { + Log.d("SnapshotDiffing", "X: ${it[0]}, Y: ${it[1]}, Z: ${it[2]}") + } } -// Convert the color to the CIELAB color space +// Convert the color to the CIE LAB color space +// http://www.brucelindbloom.com/index.html?Calc.html // +@SuppressLint("NewApi") private fun Color.toLAB(): DoubleArray { - val r = (value shr 16 and 0xff) / 255.0 - val g = (value shr 8 and 0xff) / 255.0 - val b = (value and 0xff) / 255.0 + val (x, y, z) = this.toXYZ() - val x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375 - val y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750 - val z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041 + val Xr = 95.047 + val Yr = 100.0 + val Zr = 108.883 - val l1 = 116 * f(y / 1.0) - 16 - val a1 = 500 * (f(x / 0.95047) - f(y / 1.0)) - val b1 = 200 * (f(y / 1.0) - f(z / 1.08883)) + var xr = x / Xr + var yr = y / Yr + var zr = z / Zr - return doubleArrayOf(l1, a1, b1) -} + if ( xr > 0.008856 ) { + xr = xr.pow(1/3) + } else { + xr = ((7.787 * xr) + 16 / 116.0) + } -private fun f(t: Double): Double { - return if (t > 0.008856) cbrt(t) else (7.787 * t) + (16 / 116.0) + if ( yr > 0.008856 ) { + yr = yr.pow(1/3) + } else { + yr = ((7.787 * yr) + 16 / 116.0) + } + + if ( zr > 0.008856 ) + zr = zr.pow(1/3) + else + zr = ((7.787 * zr) + 16 / 116.0) + + return doubleArrayOf( + (116 * yr) - 16, + 500 * (xr - yr), + 200 * (yr - zr) + ).also { + Log.d("SnapshotDiffing", "L: ${it[0]}, A: ${it[1]}, B: ${it[2]}") + } } -// Calculates CIEDE2000 (Delta E 2000) between two colors in the CIELAB color space returning a value between 0-100 (0 means no difference, 100 means completely opposite) -// -// This is the most recent and accurate formula, which includes corrections for perceptual uniformity -// Platform difference: iOS uses CIE94 (Delta E 1994) for color difference calculations +// CalculatesDelta E (CIE 1994) between two colors in the CIE LAB color space returning a value between 0-100 (0 means no difference, 100 means completely opposite) +// http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html // private fun Color.difference(other: Color): Double { - val (L1, C1, H1) = this.toLch() - val (L2, C2, H2) = other.toLch() - - val deltaL = L2 - L1 - val meanL = (L1 + L2) / 2 - - val deltaC = C2 - C1 - val meanC = (C1 + C2) / 2 - - val deltaH = H2 - H1 - val meanH = (H1 + H2) / 2 - - val T = 1 - 0.17 * kotlin.math.cos(Math.toRadians(meanH - 30)) + - 0.24 * kotlin.math.cos(Math.toRadians(2 * meanH)) + - 0.32 * kotlin.math.cos(Math.toRadians(3 * meanH + 6)) - - 0.20 * kotlin.math.cos(Math.toRadians(4 * meanH - 63)) - - val deltaTheta = 30 * kotlin.math.exp(-((meanH - 275) / 25).pow(2.0)) - val Rc = 2 * sqrt((meanC.pow(7.0)) / (meanC.pow(7.0) + 25.0.pow(7.0))) - val Sl = 1 + (0.015 * (meanL - 50).pow(2.0)) / sqrt(20 + (meanL - 50).pow(2.0)) - val Sc = 1 + 0.045 * meanC - val Sh = 1 + 0.015 * meanC * T - val Rt = -kotlin.math.sin(Math.toRadians(2 * deltaTheta)) * Rc - - val deltaE = sqrt( - (deltaL / Sl).pow(2.0) + - (deltaC / Sc).pow(2.0) + - (deltaH / Sh).pow(2.0) + - Rt * (deltaC / Sc) * (deltaH / Sh) - ) - Log.d("SnapshotDiffing", "Delta E: $deltaE") - return deltaE + val (l1, a1, b1) = this.toLAB() + val (l2, a2, b2) = other.toLAB() + + val deltaL = l1 - l2 + + val c1 = sqrt(a1.pow(2) + b1.pow(2)) + val c2 = sqrt(a2.pow(2) + b2.pow(2)) + val deltaC = c1 - c2 + + val deltaA = a1 - a2 + val deltaB = b1 - b2 + // TODO: The value for ΔH is not actually needed. Rather, ΔH^2 is needed instead. So an optimization might be to avoid the square root altogether. + // + val deltaH = sqrt(max(deltaA.pow(2) + deltaB.pow(2) - deltaC.pow(2), 0.0)) + + val sl = 1 + val kl = 1 + val kc = 1 + val kh = 1 + val k1 = 0.045 + val k2 = 0.015 + + val sc = 1 + k1 * c1 + val sh = 1 + k2 * c1 + + return sqrt( + (deltaL / (kl * sl)).pow(2) + + (deltaC / (kc * sc)).pow(2) + + (deltaH / (kh * sh)).pow(2) + ).also { + Log.d("SnapshotDiffing", "ΔE: $it") + } } \ No newline at end of file diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index 2e1c655..19d8856 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -6,10 +6,11 @@ import android.graphics.Color as AndroidColor private var maximumDeltaE: Double? = null + fun Diffing.Companion.bitmap( colorDiffing: Diffing, - tolerance: Double = 0.0, - perceptualTolerance: Double = 0.0 + tolerance: Double = 0.0, // 0.0 means exact match, 1.0 means completely different + perceptualTolerance: Double = 0.0 // 0.0 means exact match, 1.0 means completely different ) = Diffing { first, second -> val difference = first.differenceTo(second, perceptualTolerance) @@ -54,8 +55,8 @@ private fun Bitmap.differenceTo(other: Bitmap, perceptualTolerance: Double): Dou val deltaEPixels = thisPixels .zip(otherPixels, Color::deltaE) - val deltaEDifference = deltaEPixels.count { it < perceptualTolerance } + val pixelDifferenceCount = deltaEPixels.count { it > (perceptualTolerance * 100) } maximumDeltaE = deltaEPixels.maxOrNull() ?: 0.0 - return deltaEDifference.toDouble() / thisPixels.size + return pixelDifferenceCount.toDouble() / thisPixels.size } \ No newline at end of file From e6dd7c4cf07489389a6bf0b7086c63600406ce0a Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Mon, 21 Oct 2024 15:38:46 -0700 Subject: [PATCH 06/26] Math --- .../kotlin/com/quickbird/snapshot/Color.kt | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index a364a05..e00e247 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -5,6 +5,7 @@ import android.graphics.Color as AndroidColor import android.util.Log import androidx.annotation.ColorInt import kotlin.collections.component1 +import kotlin.math.abs import kotlin.math.max import kotlin.math.min import kotlin.math.pow @@ -40,20 +41,20 @@ private fun Color.toXYZ(): DoubleArray { } g = if (g > 0.04045) { - ((g + 0.055) / 1.055).pow(2.4); + ((g + 0.055) / 1.055).pow(2.4) } else { g / 12.92; } b = if (b > 0.04045) { - ((b + 0.055) / 1.055).pow(2.4); + ((b + 0.055) / 1.055).pow(2.4) } else { b / 12.92; } - r *= 100; - g *= 100; - b *= 100; + r *= 100 + g *= 100 + b *= 100 Log.d("SnapshotDiffing", "R: $r, G: $g, B: $b"); return doubleArrayOf( @@ -80,22 +81,23 @@ private fun Color.toLAB(): DoubleArray { var yr = y / Yr var zr = z / Zr - if ( xr > 0.008856 ) { - xr = xr.pow(1/3) + xr = if ( xr > 0.008856 ) { + xr.pow(1/3) } else { - xr = ((7.787 * xr) + 16 / 116.0) + ((7.787 * xr) + 16 / 116.0) } - if ( yr > 0.008856 ) { - yr = yr.pow(1/3) + yr = if ( yr > 0.008856 ) { + yr.pow(1/3) } else { - yr = ((7.787 * yr) + 16 / 116.0) + ((7.787 * yr) + 16 / 116.0) } - if ( zr > 0.008856 ) - zr = zr.pow(1/3) - else - zr = ((7.787 * zr) + 16 / 116.0) + zr = if ( zr > 0.008856 ) { + zr.pow(1 / 3) + } else { + ((7.787 * zr) + 16 / 116.0) + } return doubleArrayOf( (116 * yr) - 16, @@ -121,9 +123,7 @@ private fun Color.difference(other: Color): Double { val deltaA = a1 - a2 val deltaB = b1 - b2 - // TODO: The value for ΔH is not actually needed. Rather, ΔH^2 is needed instead. So an optimization might be to avoid the square root altogether. - // - val deltaH = sqrt(max(deltaA.pow(2) + deltaB.pow(2) - deltaC.pow(2), 0.0)) + val deltaH = sqrt(abs(deltaA.pow(2) + deltaB.pow(2) - deltaC.pow(2))) val sl = 1 val kl = 1 From 30c5c7b29700c43f59b0a6f1fe54f7a8a66fb812 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Mon, 21 Oct 2024 15:54:50 -0700 Subject: [PATCH 07/26] Math --- .../kotlin/com/quickbird/snapshot/Color.kt | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index e00e247..d51bc42 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -81,28 +81,31 @@ private fun Color.toLAB(): DoubleArray { var yr = y / Yr var zr = z / Zr - xr = if ( xr > 0.008856 ) { + val e = 0.008856 + val k = 903.3 + + val fx = if ( xr > e ) { xr.pow(1/3) } else { - ((7.787 * xr) + 16 / 116.0) + ((k * xr) + 16 / 116.0) } - yr = if ( yr > 0.008856 ) { + val fy = if ( yr > e ) { yr.pow(1/3) } else { - ((7.787 * yr) + 16 / 116.0) + ((k * yr) + 16 / 116.0) } - zr = if ( zr > 0.008856 ) { + val fz = if ( zr > e ) { zr.pow(1 / 3) } else { - ((7.787 * zr) + 16 / 116.0) + ((k * zr) + 16 / 116.0) } return doubleArrayOf( - (116 * yr) - 16, - 500 * (xr - yr), - 200 * (yr - zr) + (116 * fy) - 16, + 500 * (fx - fy), + 200 * (fy - fz) ).also { Log.d("SnapshotDiffing", "L: ${it[0]}, A: ${it[1]}, B: ${it[2]}") } From 351a7107e0f5f8b62d2584f1d579ab4328d5b6cc Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Mon, 21 Oct 2024 19:00:30 -0700 Subject: [PATCH 08/26] Math --- .../kotlin/com/quickbird/snapshot/Color.kt | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index d51bc42..d01b62a 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -33,30 +33,26 @@ private fun Color.toXYZ(): DoubleArray { var r = AndroidColor.red(this.value) / 255.0 var g = AndroidColor.green(this.value) / 255.0 var b = AndroidColor.blue(this.value) / 255.0 + Log.d("SnapshotDiffing", "R: $r, G: $g, B: $b") - r = if (r > 0.04045) { - ((r + 0.055) / 1.055).pow(2.4) + r = if (r <= 0.04045) { + r / 12.92 } else { - r / 12.92; + ((r + 0.055) / 1.055).pow(2.4) } g = if (g > 0.04045) { ((g + 0.055) / 1.055).pow(2.4) } else { - g / 12.92; + g / 12.92 } b = if (b > 0.04045) { ((b + 0.055) / 1.055).pow(2.4) } else { - b / 12.92; + b / 12.92 } - - r *= 100 - g *= 100 - b *= 100 - Log.d("SnapshotDiffing", "R: $r, G: $g, B: $b"); - + return doubleArrayOf( (0.4124 * r + 0.3576 * g + 0.1805 * b), (0.2126 * r + 0.7152 * g + 0.0722 * b), @@ -85,13 +81,13 @@ private fun Color.toLAB(): DoubleArray { val k = 903.3 val fx = if ( xr > e ) { - xr.pow(1/3) + xr.pow(1 / 3) } else { ((k * xr) + 16 / 116.0) } val fy = if ( yr > e ) { - yr.pow(1/3) + yr.pow(1 / 3) } else { ((k * yr) + 16 / 116.0) } From 092f899bb5e2ee31ceb6a00fd77cde7af7473dc2 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 11:35:55 -0700 Subject: [PATCH 09/26] More math --- .../kotlin/com/quickbird/snapshot/Color.kt | 26 ++++++++++++------- .../com/quickbird/snapshot/Diffing+bitmap.kt | 19 +++++++++++--- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index d01b62a..eadefa2 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -6,7 +6,6 @@ import android.util.Log import androidx.annotation.ColorInt import kotlin.collections.component1 import kotlin.math.abs -import kotlin.math.max import kotlin.math.min import kotlin.math.pow import kotlin.math.sqrt @@ -25,16 +24,21 @@ fun Color.deltaE(other: Color): Double { return min(this.difference(other) / 100, 1.0) } -// Convert the color to the CIE XYZ color space +// Convert the color to the CIE XYZ color space within nominal range of [0.0, 1.0] +// using sRGB color space and D65 white reference white // http://www.brucelindbloom.com/index.html?Calc.html // @SuppressLint("NewApi") private fun Color.toXYZ(): DoubleArray { + // Values must be within nominal range of [0.0, 1.0] + // var r = AndroidColor.red(this.value) / 255.0 var g = AndroidColor.green(this.value) / 255.0 var b = AndroidColor.blue(this.value) / 255.0 Log.d("SnapshotDiffing", "R: $r, G: $g, B: $b") + // Inverse sRGB Companding + // r = if (r <= 0.04045) { r / 12.92 } else { @@ -52,11 +56,13 @@ private fun Color.toXYZ(): DoubleArray { } else { b / 12.92 } - + + // Linear RGB to XYZ using sRGB color space and D65 white reference white + // return doubleArrayOf( - (0.4124 * r + 0.3576 * g + 0.1805 * b), - (0.2126 * r + 0.7152 * g + 0.0722 * b), - (0.0193 * r + 0.1192 * g + 0.9505 * b) + (0.4124564 * r + 0.3575761 * g + 0.1804375 * b), + (0.2126729 * r + 0.7151522 * g + 0.0721750 * b), + (0.0193339 * r + 0.1191920 * g + 0.9503041 * b) ).also { Log.d("SnapshotDiffing", "X: ${it[0]}, Y: ${it[1]}, Z: ${it[2]}") } @@ -69,9 +75,11 @@ private fun Color.toXYZ(): DoubleArray { private fun Color.toLAB(): DoubleArray { val (x, y, z) = this.toXYZ() - val Xr = 95.047 - val Yr = 100.0 - val Zr = 108.883 + // CIE standard illuminant D65 + // + val Xr = 0.9504 + val Yr = 1.000 + val Zr = 1.0888 var xr = x / Xr var yr = y / Yr diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index 19d8856..8c33479 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -53,10 +53,21 @@ private fun Bitmap.differenceTo(other: Bitmap, perceptualTolerance: Double): Dou val otherPixels = other.pixels if (thisPixels.size != otherPixels.size) return 100.0 - val deltaEPixels = thisPixels - .zip(otherPixels, Color::deltaE) - val pixelDifferenceCount = deltaEPixels.count { it > (perceptualTolerance * 100) } - maximumDeltaE = deltaEPixels.maxOrNull() ?: 0.0 + // Perceptually compare if the tolerance is greater than 0.0 + // + val pixelDifferenceCount = if (perceptualTolerance > 0.0) { + val deltaEPixels = thisPixels + .zip(otherPixels, Color::deltaE) + // Perceptual tolerance is given in range of 0.0 - 1.0, this needs to be scaled + // when comparing against Delta E values between 0 (same) - 100 (completely different) + // + maximumDeltaE = deltaEPixels.maxOrNull() ?: 0.0 + deltaEPixels.count { it > (perceptualTolerance * 100) } + } else { + thisPixels + .zip(otherPixels, Color::equals) + .count { !it } + } return pixelDifferenceCount.toDouble() / thisPixels.size } \ No newline at end of file From 9a10c469d93a261d713ee01ca8e24e06be971bed Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 12:21:02 -0700 Subject: [PATCH 10/26] Math --- .../kotlin/com/quickbird/snapshot/Color.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index eadefa2..6c2a678 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -6,6 +6,7 @@ import android.util.Log import androidx.annotation.ColorInt import kotlin.collections.component1 import kotlin.math.abs +import kotlin.math.cbrt import kotlin.math.min import kotlin.math.pow import kotlin.math.sqrt @@ -89,25 +90,25 @@ private fun Color.toLAB(): DoubleArray { val k = 903.3 val fx = if ( xr > e ) { - xr.pow(1 / 3) + cbrt(xr) } else { - ((k * xr) + 16 / 116.0) + ((k * xr) + 16) / 116.0 } val fy = if ( yr > e ) { - yr.pow(1 / 3) + cbrt(yr) } else { - ((k * yr) + 16 / 116.0) + ((k * yr) + 16) / 116.0 } val fz = if ( zr > e ) { - zr.pow(1 / 3) + cbrt(zr) } else { - ((k * zr) + 16 / 116.0) + ((k * zr) + 16) / 116.0 } return doubleArrayOf( - (116 * fy) - 16, + 116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz) ).also { From cde72ba6a485f94244f32dfe99409b7748cfca99 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 12:36:17 -0700 Subject: [PATCH 11/26] Comment out logs --- .../androidMain/kotlin/com/quickbird/snapshot/Color.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index 6c2a678..01f4294 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -36,7 +36,7 @@ private fun Color.toXYZ(): DoubleArray { var r = AndroidColor.red(this.value) / 255.0 var g = AndroidColor.green(this.value) / 255.0 var b = AndroidColor.blue(this.value) / 255.0 - Log.d("SnapshotDiffing", "R: $r, G: $g, B: $b") + // Log.d("SnapshotDiffing", "R: $r, G: $g, B: $b") // Inverse sRGB Companding // @@ -65,7 +65,7 @@ private fun Color.toXYZ(): DoubleArray { (0.2126729 * r + 0.7151522 * g + 0.0721750 * b), (0.0193339 * r + 0.1191920 * g + 0.9503041 * b) ).also { - Log.d("SnapshotDiffing", "X: ${it[0]}, Y: ${it[1]}, Z: ${it[2]}") + // Log.d("SnapshotDiffing", "X: ${it[0]}, Y: ${it[1]}, Z: ${it[2]}") } } @@ -112,7 +112,7 @@ private fun Color.toLAB(): DoubleArray { 500 * (fx - fy), 200 * (fy - fz) ).also { - Log.d("SnapshotDiffing", "L: ${it[0]}, A: ${it[1]}, B: ${it[2]}") + // Log.d("SnapshotDiffing", "L: ${it[0]}, A: ${it[1]}, B: ${it[2]}") } } @@ -148,6 +148,6 @@ private fun Color.difference(other: Color): Double { (deltaC / (kc * sc)).pow(2) + (deltaH / (kh * sh)).pow(2) ).also { - Log.d("SnapshotDiffing", "ΔE: $it") + // Log.d("SnapshotDiffing", "ΔE: $it") } } \ No newline at end of file From 52e5311c275079b9c4169aefdfacdc347662506d Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 13:00:09 -0700 Subject: [PATCH 12/26] Log --- .../kotlin/com/quickbird/snapshot/Diffing+bitmap.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index 8c33479..97fa034 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -15,10 +15,11 @@ fun Diffing.Companion.bitmap( val difference = first.differenceTo(second, perceptualTolerance) if (difference <= tolerance) { + Log.d("SnapshotDiffing", "Actual image difference ${difference.toString()}, required image difference ${tolerance.toString()}") null } else { - var log = "Actual image tolerance $difference is greater than required $tolerance" - if (maximumDeltaE != null) log += ", Actual perceptual tolerance $maximumDeltaE is greater than required $perceptualTolerance" + var log = "Actual image difference ${difference.toString()} is greater than max allowed ${tolerance.toString()}" + if (maximumDeltaE != null) log += ", Actual perceptual difference ${maximumDeltaE.toString()} is greater than max allowed ${perceptualTolerance.toString()}" Log.e("SnapshotDiffing", log) first.copy(first.config!!, true).apply { From 4ac5bd098ea3e6c484747465f58dee1e70ab0810 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 13:32:31 -0700 Subject: [PATCH 13/26] Update Diffing+bitmap.kt --- .../kotlin/com/quickbird/snapshot/Diffing+bitmap.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index 97fa034..15baac6 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -52,18 +52,19 @@ val Diffing.Companion.intMean private fun Bitmap.differenceTo(other: Bitmap, perceptualTolerance: Double): Double { val thisPixels = this.pixels val otherPixels = other.pixels - if (thisPixels.size != otherPixels.size) return 100.0 + if (thisPixels.size != otherPixels.size) return 1.0 // Perceptually compare if the tolerance is greater than 0.0 // val pixelDifferenceCount = if (perceptualTolerance > 0.0) { val deltaEPixels = thisPixels .zip(otherPixels, Color::deltaE) - // Perceptual tolerance is given in range of 0.0 - 1.0, this needs to be scaled - // when comparing against Delta E values between 0 (same) - 100 (completely different) + // Perceptual tolerance is given in range of 0.0 (same) - 1.0 (completely different) that + // needs to be scaled when comparing against Delta E values between 0 (same) - 100 (completely different) // + Log.d("SnapshotDiffing", deltaEPixels.toString()) maximumDeltaE = deltaEPixels.maxOrNull() ?: 0.0 - deltaEPixels.count { it > (perceptualTolerance * 100) } + deltaEPixels.count { it > (perceptualTolerance) } } else { thisPixels .zip(otherPixels, Color::equals) From c53f8c974d7ca1b893872551149ea76e19876529 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 13:59:11 -0700 Subject: [PATCH 14/26] Update Diffing+bitmap.kt --- .../kotlin/com/quickbird/snapshot/Diffing+bitmap.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index 15baac6..85285e6 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -2,6 +2,7 @@ package com.quickbird.snapshot import android.graphics.Bitmap import android.util.Log +import java.io.File import android.graphics.Color as AndroidColor private var maximumDeltaE: Double? = null @@ -62,7 +63,9 @@ private fun Bitmap.differenceTo(other: Bitmap, perceptualTolerance: Double): Dou // Perceptual tolerance is given in range of 0.0 (same) - 1.0 (completely different) that // needs to be scaled when comparing against Delta E values between 0 (same) - 100 (completely different) // - Log.d("SnapshotDiffing", deltaEPixels.toString()) + File("somefile.txt").printWriter().use { out -> + out.println(deltaEPixels.toString()) + } maximumDeltaE = deltaEPixels.maxOrNull() ?: 0.0 deltaEPixels.count { it > (perceptualTolerance) } } else { From 9b0b359af74e1ccfe84b667c2bafe83a325a8727 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 14:06:11 -0700 Subject: [PATCH 15/26] Update Diffing+bitmap.kt --- .../kotlin/com/quickbird/snapshot/Diffing+bitmap.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index 85285e6..48aed62 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -2,6 +2,7 @@ package com.quickbird.snapshot import android.graphics.Bitmap import android.util.Log +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import java.io.File import android.graphics.Color as AndroidColor @@ -63,7 +64,8 @@ private fun Bitmap.differenceTo(other: Bitmap, perceptualTolerance: Double): Dou // Perceptual tolerance is given in range of 0.0 (same) - 1.0 (completely different) that // needs to be scaled when comparing against Delta E values between 0 (same) - 100 (completely different) // - File("somefile.txt").printWriter().use { out -> + File(getInstrumentation().targetContext.filesDir.canonicalPath + + File.separator + "assets/somefile.txt").printWriter().use { out -> out.println(deltaEPixels.toString()) } maximumDeltaE = deltaEPixels.maxOrNull() ?: 0.0 From b0fe09d64b1e08d58c0a35aa890c54e15dc9e226 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 14:27:47 -0700 Subject: [PATCH 16/26] Update Diffing+bitmap.kt --- .../kotlin/com/quickbird/snapshot/Diffing+bitmap.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index 48aed62..70f5911 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -2,8 +2,6 @@ package com.quickbird.snapshot import android.graphics.Bitmap import android.util.Log -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import java.io.File import android.graphics.Color as AndroidColor private var maximumDeltaE: Double? = null @@ -64,11 +62,12 @@ private fun Bitmap.differenceTo(other: Bitmap, perceptualTolerance: Double): Dou // Perceptual tolerance is given in range of 0.0 (same) - 1.0 (completely different) that // needs to be scaled when comparing against Delta E values between 0 (same) - 100 (completely different) // - File(getInstrumentation().targetContext.filesDir.canonicalPath + - File.separator + "assets/somefile.txt").printWriter().use { out -> - out.println(deltaEPixels.toString()) - } + val minimumDeltaE = deltaEPixels.minOrNull() ?: 0.0 maximumDeltaE = deltaEPixels.maxOrNull() ?: 0.0 + val average = deltaEPixels.average() + val count = deltaEPixels.count() + val size = deltaEPixels.size + Log.e("SnapshotDiffing", "Minimum Delta E: $minimumDeltaE, Maximum Delta E: $maximumDeltaE, Average Delta E: $average, Count: $count, Size: $size") deltaEPixels.count { it > (perceptualTolerance) } } else { thisPixels From ff951f5fe0b2b727cee33b0a6547f14d0d689c80 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 15:12:15 -0700 Subject: [PATCH 17/26] Remove logs add docstrings --- .../kotlin/com/quickbird/snapshot/Color.kt | 48 +++++++++---------- .../com/quickbird/snapshot/Diffing+bitmap.kt | 17 ++++--- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index 01f4294..8d0701a 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -16,29 +16,31 @@ data class Color(@ColorInt val value: Int) val @receiver:ColorInt Int.color get() = Color(this) +/** + * Calculates [Delta E (1994)](http://zschuessler.github.io/DeltaE/learn/#toc-delta-e-94) between + * two colors in the CIE LAB color space returning a value between 0.0 - 1.0 (0.0 means no difference, 1.0 means completely opposite) + */ fun Color.deltaE(other: Color): Double { if (this == other) { return 0.0 } - // Compute the Delta E 2000 difference between the two colors in the CIE Lch color space and return whether it's within the perceptual tolerance + // Delta E (1994) is in a 0-100 scale, so we need to divide by 100 to transform it to a percentage // - return min(this.difference(other) / 100, 1.0) + return min(this.deltaE1994(other) / 100, 1.0) } -// Convert the color to the CIE XYZ color space within nominal range of [0.0, 1.0] -// using sRGB color space and D65 white reference white -// http://www.brucelindbloom.com/index.html?Calc.html -// -@SuppressLint("NewApi") +/** + * Convert the color to the CIE XYZ color space within nominal range of [0.0, 1.0] + * using sRGB color space and D65 white reference white + */ private fun Color.toXYZ(): DoubleArray { // Values must be within nominal range of [0.0, 1.0] // var r = AndroidColor.red(this.value) / 255.0 var g = AndroidColor.green(this.value) / 255.0 var b = AndroidColor.blue(this.value) / 255.0 - // Log.d("SnapshotDiffing", "R: $r, G: $g, B: $b") - // Inverse sRGB Companding + // Inverse sRGB companding // r = if (r <= 0.04045) { r / 12.92 @@ -64,15 +66,12 @@ private fun Color.toXYZ(): DoubleArray { (0.4124564 * r + 0.3575761 * g + 0.1804375 * b), (0.2126729 * r + 0.7151522 * g + 0.0721750 * b), (0.0193339 * r + 0.1191920 * g + 0.9503041 * b) - ).also { - // Log.d("SnapshotDiffing", "X: ${it[0]}, Y: ${it[1]}, Z: ${it[2]}") - } + ) } -// Convert the color to the CIE LAB color space -// http://www.brucelindbloom.com/index.html?Calc.html -// -@SuppressLint("NewApi") +/** + * Convert the color to the CIE LAB color space using sRGB color space and D65 white reference white + */ private fun Color.toLAB(): DoubleArray { val (x, y, z) = this.toXYZ() @@ -111,15 +110,14 @@ private fun Color.toLAB(): DoubleArray { 116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz) - ).also { - // Log.d("SnapshotDiffing", "L: ${it[0]}, A: ${it[1]}, B: ${it[2]}") - } + ) } -// CalculatesDelta E (CIE 1994) between two colors in the CIE LAB color space returning a value between 0-100 (0 means no difference, 100 means completely opposite) -// http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html -// -private fun Color.difference(other: Color): Double { +/** + * Calculates [Delta E (1994)](http://zschuessler.github.io/DeltaE/learn/#toc-delta-e-94) between + * two colors in the CIE LAB color space returning a value between 0-100 (0 means no difference, 100 means completely opposite) + */ +private fun Color.deltaE1994(other: Color): Double { val (l1, a1, b1) = this.toLAB() val (l2, a2, b2) = other.toLAB() @@ -147,7 +145,5 @@ private fun Color.difference(other: Color): Double { (deltaL / (kl * sl)).pow(2) + (deltaC / (kc * sc)).pow(2) + (deltaH / (kh * sh)).pow(2) - ).also { - // Log.d("SnapshotDiffing", "ΔE: $it") - } + ) } \ No newline at end of file diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index 70f5911..d6b07ce 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -6,11 +6,19 @@ import android.graphics.Color as AndroidColor private var maximumDeltaE: Double? = null - +/** + * A Bitmap comparison diffing strategy for comparing images based on pixel equality. + * + * @param colorDiffing A function that compares two colors and returns a color representing the difference. + * @param tolerance Total percentage of pixels that must match between image. The default value of 0.0% means all pixels must match + * @param perceptualTolerance Percentage each pixel can be different from source pixel and still considered + * a match. The default value of 0.0% means pixels must match perfectly whereas the recommended value of 0.02% mimics the + * [precision](http://zschuessler.github.io/DeltaE/learn/#toc-defining-delta-e) of the human eye. + */ fun Diffing.Companion.bitmap( colorDiffing: Diffing, tolerance: Double = 0.0, // 0.0 means exact match, 1.0 means completely different - perceptualTolerance: Double = 0.0 // 0.0 means exact match, 1.0 means completely different + perceptualTolerance: Double = 0.0 // 0.0 means exact match, 1.0 means allow pixels to be completely different ) = Diffing { first, second -> val difference = first.differenceTo(second, perceptualTolerance) @@ -62,12 +70,7 @@ private fun Bitmap.differenceTo(other: Bitmap, perceptualTolerance: Double): Dou // Perceptual tolerance is given in range of 0.0 (same) - 1.0 (completely different) that // needs to be scaled when comparing against Delta E values between 0 (same) - 100 (completely different) // - val minimumDeltaE = deltaEPixels.minOrNull() ?: 0.0 maximumDeltaE = deltaEPixels.maxOrNull() ?: 0.0 - val average = deltaEPixels.average() - val count = deltaEPixels.count() - val size = deltaEPixels.size - Log.e("SnapshotDiffing", "Minimum Delta E: $minimumDeltaE, Maximum Delta E: $maximumDeltaE, Average Delta E: $average, Count: $count, Size: $size") deltaEPixels.count { it > (perceptualTolerance) } } else { thisPixels From 5295c179fcdb77b0c01e5dc0a23672e92662f884 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 15:13:11 -0700 Subject: [PATCH 18/26] Update Color.kt --- snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index 8d0701a..3500fc4 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -1,8 +1,6 @@ package com.quickbird.snapshot -import android.annotation.SuppressLint import android.graphics.Color as AndroidColor -import android.util.Log import androidx.annotation.ColorInt import kotlin.collections.component1 import kotlin.math.abs From 6b758bc51660acb9e25ce6253627d00674be864e Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 17:21:19 -0700 Subject: [PATCH 19/26] Log format --- .../kotlin/com/quickbird/snapshot/Diffing+bitmap.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index d6b07ce..f8a9e60 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -23,11 +23,11 @@ fun Diffing.Companion.bitmap( val difference = first.differenceTo(second, perceptualTolerance) if (difference <= tolerance) { - Log.d("SnapshotDiffing", "Actual image difference ${difference.toString()}, required image difference ${tolerance.toString()}") + Log.d("SnapshotDiffing", "Actual image difference ${difference.toBigDecimal().toPlainString()}, required image difference ${tolerance.toBigDecimal().toPlainString()}") null } else { - var log = "Actual image difference ${difference.toString()} is greater than max allowed ${tolerance.toString()}" - if (maximumDeltaE != null) log += ", Actual perceptual difference ${maximumDeltaE.toString()} is greater than max allowed ${perceptualTolerance.toString()}" + var log = "Actual image difference ${difference.toBigDecimal().toPlainString()} is greater than max allowed ${tolerance.toBigDecimal().toPlainString()}" + if (maximumDeltaE != null) log += ", Actual perceptual difference ${maximumDeltaE.toString()} is greater than max allowed ${perceptualTolerance.toBigDecimal().toPlainString()}" Log.e("SnapshotDiffing", log) first.copy(first.config!!, true).apply { @@ -67,8 +67,7 @@ private fun Bitmap.differenceTo(other: Bitmap, perceptualTolerance: Double): Dou val pixelDifferenceCount = if (perceptualTolerance > 0.0) { val deltaEPixels = thisPixels .zip(otherPixels, Color::deltaE) - // Perceptual tolerance is given in range of 0.0 (same) - 1.0 (completely different) that - // needs to be scaled when comparing against Delta E values between 0 (same) - 100 (completely different) + // Find the maximum delta E value for logging purposes // maximumDeltaE = deltaEPixels.maxOrNull() ?: 0.0 deltaEPixels.count { it > (perceptualTolerance) } From 565e00c4a5c75dbc358b13c27961d7c556a72108 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 18:03:10 -0700 Subject: [PATCH 20/26] Logging --- .../com/quickbird/snapshot/Diffing+bitmap.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index f8a9e60..7ccfb16 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -27,14 +27,16 @@ fun Diffing.Companion.bitmap( null } else { var log = "Actual image difference ${difference.toBigDecimal().toPlainString()} is greater than max allowed ${tolerance.toBigDecimal().toPlainString()}" - if (maximumDeltaE != null) log += ", Actual perceptual difference ${maximumDeltaE.toString()} is greater than max allowed ${perceptualTolerance.toBigDecimal().toPlainString()}" + maximumDeltaE?.let { log += ", Actual perceptual difference ${it.toBigDecimal().toPlainString()} is greater than max allowed ${perceptualTolerance.toBigDecimal().toPlainString()}" } Log.e("SnapshotDiffing", log) - first.copy(first.config!!, true).apply { - updatePixels { x, y, color -> - if (x < second.width && y < second.height) - colorDiffing(color, second.getPixel(x, y).color) ?: color - else color + first.config?.let { + first.copy(it, true).apply { + updatePixels { x, y, color -> + if (x < second.width && y < second.height) + colorDiffing(color, second.getPixel(x, y).color) ?: color + else color + } } } } From df1cf42cb726fab806afe07bb56d5a6c8e6b0a15 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Tue, 22 Oct 2024 18:23:23 -0700 Subject: [PATCH 21/26] Update Diffing+bitmap.kt --- .../kotlin/com/quickbird/snapshot/Diffing+bitmap.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index 7ccfb16..5d1a422 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -17,8 +17,8 @@ private var maximumDeltaE: Double? = null */ fun Diffing.Companion.bitmap( colorDiffing: Diffing, - tolerance: Double = 0.0, // 0.0 means exact match, 1.0 means completely different - perceptualTolerance: Double = 0.0 // 0.0 means exact match, 1.0 means allow pixels to be completely different + tolerance: Double = 0.0, + perceptualTolerance: Double = 0.0 ) = Diffing { first, second -> val difference = first.differenceTo(second, perceptualTolerance) @@ -71,7 +71,12 @@ private fun Bitmap.differenceTo(other: Bitmap, perceptualTolerance: Double): Dou .zip(otherPixels, Color::deltaE) // Find the maximum delta E value for logging purposes // + val minimumDeltaE = deltaEPixels.minOrNull() ?: 0.0 maximumDeltaE = deltaEPixels.maxOrNull() ?: 0.0 + val average = deltaEPixels.average() + val count = deltaEPixels.count() + val size = deltaEPixels.size + Log.e("SnapshotDiffing", "Minimum Delta E: $minimumDeltaE, Maximum Delta E: ${maximumDeltaE!!.toBigDecimal().toPlainString()}, Average Delta E: $average, Count: $count, Size: $size") deltaEPixels.count { it > (perceptualTolerance) } } else { thisPixels From e332e42032436c273c8f7c2b8e7c6ff9f8b13c27 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Wed, 23 Oct 2024 17:08:47 -0700 Subject: [PATCH 22/26] Remove log --- .../kotlin/com/quickbird/snapshot/Diffing+bitmap.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index 5d1a422..423e692 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -71,12 +71,7 @@ private fun Bitmap.differenceTo(other: Bitmap, perceptualTolerance: Double): Dou .zip(otherPixels, Color::deltaE) // Find the maximum delta E value for logging purposes // - val minimumDeltaE = deltaEPixels.minOrNull() ?: 0.0 maximumDeltaE = deltaEPixels.maxOrNull() ?: 0.0 - val average = deltaEPixels.average() - val count = deltaEPixels.count() - val size = deltaEPixels.size - Log.e("SnapshotDiffing", "Minimum Delta E: $minimumDeltaE, Maximum Delta E: ${maximumDeltaE!!.toBigDecimal().toPlainString()}, Average Delta E: $average, Count: $count, Size: $size") deltaEPixels.count { it > (perceptualTolerance) } } else { thisPixels From b2b051a415d481396bdaee32f0838af4c76fe239 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Wed, 23 Oct 2024 17:09:42 -0700 Subject: [PATCH 23/26] Update Diffing+bitmap.kt --- .../androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt index 423e692..f72b5c4 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Diffing+bitmap.kt @@ -30,7 +30,7 @@ fun Diffing.Companion.bitmap( maximumDeltaE?.let { log += ", Actual perceptual difference ${it.toBigDecimal().toPlainString()} is greater than max allowed ${perceptualTolerance.toBigDecimal().toPlainString()}" } Log.e("SnapshotDiffing", log) - first.config?.let { + first.config.let { first.copy(it, true).apply { updatePixels { x, y, color -> if (x < second.width && y < second.height) From 031f58d05477b3d7db1a5d2e52fe0e9862404964 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Wed, 23 Oct 2024 17:18:47 -0700 Subject: [PATCH 24/26] Revert .gitignore --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4ca26e4..00fd4dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ /build -/.gradle -.idea -local.properties +/.gradle \ No newline at end of file From d1f4d6a43ea13893bbf5b06ea6436fe08d36bf11 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Wed, 13 Nov 2024 06:40:42 -0800 Subject: [PATCH 25/26] Simplify conditional logic --- .../kotlin/com/quickbird/snapshot/Color.kt | 54 +++++-------------- 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index 3500fc4..360946a 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -40,30 +40,16 @@ private fun Color.toXYZ(): DoubleArray { // Inverse sRGB companding // - r = if (r <= 0.04045) { - r / 12.92 - } else { - ((r + 0.055) / 1.055).pow(2.4) - } - - g = if (g > 0.04045) { - ((g + 0.055) / 1.055).pow(2.4) - } else { - g / 12.92 - } - - b = if (b > 0.04045) { - ((b + 0.055) / 1.055).pow(2.4) - } else { - b / 12.92 - } + r = if (r <= 0.04045) r / 12.92 else ((r + 0.055) / 1.055).pow(2.4) + g = if (g <= 0.04045) g / 12.92 else ((g + 0.055) / 1.055).pow(2.4) + b = if (b <= 0.04045) b / 12.92 else ((b + 0.055) / 1.055).pow(2.4) // Linear RGB to XYZ using sRGB color space and D65 white reference white // return doubleArrayOf( - (0.4124564 * r + 0.3575761 * g + 0.1804375 * b), - (0.2126729 * r + 0.7151522 * g + 0.0721750 * b), - (0.0193339 * r + 0.1191920 * g + 0.9503041 * b) + 0.4124564 * r + 0.3575761 * g + 0.1804375 * b, + 0.2126729 * r + 0.7151522 * g + 0.0721750 * b, + 0.0193339 * r + 0.1191920 * g + 0.9503041 * b ) } @@ -79,30 +65,16 @@ private fun Color.toLAB(): DoubleArray { val Yr = 1.000 val Zr = 1.0888 - var xr = x / Xr - var yr = y / Yr - var zr = z / Zr + val xr = x / Xr + val yr = y / Yr + val zr = z / Zr val e = 0.008856 val k = 903.3 - val fx = if ( xr > e ) { - cbrt(xr) - } else { - ((k * xr) + 16) / 116.0 - } - - val fy = if ( yr > e ) { - cbrt(yr) - } else { - ((k * yr) + 16) / 116.0 - } - - val fz = if ( zr > e ) { - cbrt(zr) - } else { - ((k * zr) + 16) / 116.0 - } + val fx = if (xr > e) cbrt(xr) else ((k * xr) + 16) / 116.0 + val fy = if (yr > e) cbrt(yr) else ((k * yr) + 16) / 116.0 + val fz = if (zr > e) cbrt(zr) else ((k * zr) + 16) / 116.0 return doubleArrayOf( 116 * fy - 16, @@ -120,11 +92,9 @@ private fun Color.deltaE1994(other: Color): Double { val (l2, a2, b2) = other.toLAB() val deltaL = l1 - l2 - val c1 = sqrt(a1.pow(2) + b1.pow(2)) val c2 = sqrt(a2.pow(2) + b2.pow(2)) val deltaC = c1 - c2 - val deltaA = a1 - a2 val deltaB = b1 - b2 val deltaH = sqrt(abs(deltaA.pow(2) + deltaB.pow(2) - deltaC.pow(2))) From 4e29008a5e3179d4c8fcf86f33b02e67b273d0b8 Mon Sep 17 00:00:00 2001 From: Joel Whitney Date: Fri, 15 Nov 2024 07:22:14 -0800 Subject: [PATCH 26/26] Update snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt Co-authored-by: Rinzin Wangchuk <33096241+eldrig0@users.noreply.github.com> --- .../kotlin/com/quickbird/snapshot/Color.kt | 56 ++++--------------- 1 file changed, 11 insertions(+), 45 deletions(-) diff --git a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt index 360946a..a581517 100644 --- a/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt +++ b/snapshot/src/androidMain/kotlin/com/quickbird/snapshot/Color.kt @@ -31,56 +31,22 @@ fun Color.deltaE(other: Color): Double { * Convert the color to the CIE XYZ color space within nominal range of [0.0, 1.0] * using sRGB color space and D65 white reference white */ -private fun Color.toXYZ(): DoubleArray { - // Values must be within nominal range of [0.0, 1.0] - // - var r = AndroidColor.red(this.value) / 255.0 - var g = AndroidColor.green(this.value) / 255.0 - var b = AndroidColor.blue(this.value) / 255.0 - - // Inverse sRGB companding - // - r = if (r <= 0.04045) r / 12.92 else ((r + 0.055) / 1.055).pow(2.4) - g = if (g <= 0.04045) g / 12.92 else ((g + 0.055) / 1.055).pow(2.4) - b = if (b <= 0.04045) b / 12.92 else ((b + 0.055) / 1.055).pow(2.4) - - // Linear RGB to XYZ using sRGB color space and D65 white reference white - // - return doubleArrayOf( - 0.4124564 * r + 0.3575761 * g + 0.1804375 * b, - 0.2126729 * r + 0.7151522 * g + 0.0721750 * b, - 0.0193339 * r + 0.1191920 * g + 0.9503041 * b - ) -} - /** * Convert the color to the CIE LAB color space using sRGB color space and D65 white reference white */ -private fun Color.toLAB(): DoubleArray { - val (x, y, z) = this.toXYZ() - - // CIE standard illuminant D65 - // - val Xr = 0.9504 - val Yr = 1.000 - val Zr = 1.0888 - - val xr = x / Xr - val yr = y / Yr - val zr = z / Zr - - val e = 0.008856 - val k = 903.3 - - val fx = if (xr > e) cbrt(xr) else ((k * xr) + 16) / 116.0 - val fy = if (yr > e) cbrt(yr) else ((k * yr) + 16) / 116.0 - val fz = if (zr > e) cbrt(zr) else ((k * zr) + 16) / 116.0 +private fun Color.toLAB(): FloatArray { + val labConnector = ColorSpace.connect( + ColorSpace.get(ColorSpace.Named.SRGB), + ColorSpace.get(ColorSpace.Named.CIE_LAB) + ) - return doubleArrayOf( - 116 * fy - 16, - 500 * (fx - fy), - 200 * (fy - fz) + val rgb = floatArrayOf( + AndroidColor.red(value) / 255.0f, + AndroidColor.green(value) / 255.0f, + AndroidColor.blue(value) / 255.0f ) + + return labConnector.transform(rgb[0], rgb[1], rgb[2]) } /**