Skip to content

Commit a6c0adb

Browse files
committed
FuzzyCompare uses function reference and caches configuration to avoid calculations inside the loop
1 parent 6f4a509 commit a6c0adb

File tree

3 files changed

+68
-42
lines changed

3 files changed

+68
-42
lines changed

Library/src/main/java/dev/testify/core/processor/ParallelPixelProcessor.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import java.nio.IntBuffer
3434
import java.util.BitSet
3535
import kotlin.math.ceil
3636

37+
typealias AnalyzePixelFunction = (baselinePixel: Int, currentPixel: Int, position: Pair<Int, Int>) -> Boolean
38+
3739
/**
3840
* A class that allows for parallel processing of pixels in a bitmap.
3941
*
@@ -136,7 +138,7 @@ class ParallelPixelProcessor private constructor(
136138
* @param analyzer The analyzer function to call for each pixel.
137139
* @return True if all pixels pass the analyzer function, false otherwise.
138140
*/
139-
fun analyze(analyzer: (baselinePixel: Int, currentPixel: Int, position: Pair<Int, Int>) -> Boolean): Boolean {
141+
fun analyze(analyzer: AnalyzePixelFunction): Boolean {
140142
val (width, height, baselineBuffer, currentBuffer) = prepareBuffers()
141143

142144
val chunkData = getChunkData(width, height)

Library/src/main/java/dev/testify/core/processor/compare/FuzzyCompare.kt

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package dev.testify.core.processor.compare
2626

2727
import android.graphics.Bitmap
28+
import android.graphics.Rect
2829
import com.github.ajalt.colormath.RGB
2930
import dev.testify.core.TestifyConfiguration
3031
import dev.testify.core.processor.ParallelPixelProcessor
@@ -43,9 +44,30 @@ import dev.testify.core.processor.compare.colorspace.calculateDeltaE
4344
* @param parallelProcessorConfiguration - The configuration for the [ParallelPixelProcessor].
4445
*/
4546
internal class FuzzyCompare(
46-
private val configuration: TestifyConfiguration,
47+
configuration: TestifyConfiguration,
4748
private val parallelProcessorConfiguration: ParallelProcessorConfiguration = ParallelProcessorConfiguration()
4849
) {
50+
private val exclusionRects: Set<Rect> = configuration.exclusionRects
51+
private val exactness: Float = configuration.exactness ?: 1f
52+
53+
private val analyzePixelFunction: (baselinePixel: Int, currentPixel: Int) -> Boolean =
54+
if (configuration.hasExactness) { baselinePixel, currentPixel ->
55+
val baselineLab = RGB.fromInt(baselinePixel).toLAB()
56+
val currentLab = RGB.fromInt(currentPixel).toLAB()
57+
58+
val deltaE = calculateDeltaE(
59+
baselineLab.l,
60+
baselineLab.a,
61+
baselineLab.b,
62+
currentLab.l,
63+
currentLab.a,
64+
currentLab.b
65+
)
66+
((100.0 - deltaE) / 100.0f >= exactness)
67+
}
68+
else { baselinePixel, currentPixel ->
69+
baselinePixel == currentPixel
70+
}
4971

5072
fun compareBitmaps(baselineBitmap: Bitmap, currentBitmap: Bitmap): Boolean {
5173
if (baselineBitmap.height != currentBitmap.height) {
@@ -69,31 +91,13 @@ internal class FuzzyCompare(
6991
/* return */ true
7092
} else {
7193
var exclude = false
72-
for (rect in configuration.exclusionRects) {
94+
for (rect in exclusionRects) {
7395
if (rect.contains(x, y)) {
7496
exclude = true
7597
break
7698
}
7799
}
78-
when {
79-
exclude -> true // return ^analyze
80-
configuration.hasExactness -> {
81-
val baselineLab = RGB.fromInt(baselinePixel).toLAB()
82-
val currentLab = RGB.fromInt(currentPixel).toLAB()
83-
84-
val deltaE = calculateDeltaE(
85-
baselineLab.l,
86-
baselineLab.a,
87-
baselineLab.b,
88-
currentLab.l,
89-
currentLab.a,
90-
currentLab.b
91-
)
92-
((100.0 - deltaE) / 100.0f >= configuration.exactness!!) // return ^analyze
93-
}
94-
95-
else -> baselinePixel == currentPixel // return ^analyze
96-
}
100+
exclude || analyzePixelFunction(baselinePixel, currentPixel)
97101
}
98102
}
99103
}

Library/src/test/java/dev/testify/core/processor/ParallelPixelProcessorTest.kt

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
*/
2424
package dev.testify.core.processor
2525

26+
import android.graphics.Bitmap
2627
import kotlinx.coroutines.DelicateCoroutinesApi
2728
import kotlinx.coroutines.Dispatchers
2829
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -48,11 +49,15 @@ class ParallelPixelProcessorTest {
4849
}
4950
}
5051

51-
fun setUp(maxNumberOfChunkThreads: Int? = null): ParallelPixelProcessor {
52+
private fun setUp(
53+
maxNumberOfChunkThreads: Int? = null,
54+
baseline: Bitmap = mockBitmap(),
55+
current: Bitmap = mockBitmap()
56+
): ParallelPixelProcessor {
5257
return ParallelPixelProcessor
5358
.create(forceSingleThreadedExecution(maxNumberOfChunkThreads))
54-
.baseline(mockBitmap())
55-
.current(mockBitmap())
59+
.baseline(baseline)
60+
.current(current)
5661
}
5762

5863
@After
@@ -62,45 +67,45 @@ class ParallelPixelProcessorTest {
6267
}
6368

6469
@Test
65-
fun default() {
70+
fun `WHEN bitmap is processed THEN analyze every pixel`() {
6671
val pixelProcessor = setUp(maxNumberOfChunkThreads = 1)
6772

68-
val index = AtomicInteger(0)
73+
val analyzed = AtomicInteger(0)
6974
pixelProcessor.analyze { _, _, _ ->
70-
index.incrementAndGet()
75+
analyzed.incrementAndGet()
7176
true
7277
}
73-
assertEquals(DEFAULT_BITMAP_WIDTH * DEFAULT_BITMAP_HEIGHT, index.get())
78+
assertEquals(DEFAULT_BITMAP_WIDTH * DEFAULT_BITMAP_HEIGHT, analyzed.get())
7479
}
7580

7681
@Test
77-
fun twoCores() {
82+
fun `WHEN processor has two threads THEN analyze every pixel`() {
7883
val pixelProcessor = setUp(maxNumberOfChunkThreads = 2)
7984

80-
val index = AtomicInteger(0)
85+
val analyzed = AtomicInteger(0)
8186
pixelProcessor.analyze { _, _, _ ->
82-
index.incrementAndGet()
87+
analyzed.incrementAndGet()
8388
true
8489
}
8590

86-
assertEquals(DEFAULT_BITMAP_WIDTH * DEFAULT_BITMAP_HEIGHT, index.get())
91+
assertEquals(DEFAULT_BITMAP_WIDTH * DEFAULT_BITMAP_HEIGHT, analyzed.get())
8792
}
8893

8994
@Test
90-
fun oddNumberOfCores() {
95+
fun `WHEN there are an odd number of threads THEN analyze every pixel`() {
9196
val pixelProcessor = setUp(maxNumberOfChunkThreads = 7)
9297

93-
val index = AtomicInteger(0)
98+
val analyzed = AtomicInteger(0)
9499
pixelProcessor.analyze { _, _, _ ->
95-
index.incrementAndGet()
100+
analyzed.incrementAndGet()
96101
true
97102
}
98103

99-
assertEquals(DEFAULT_BITMAP_WIDTH * DEFAULT_BITMAP_HEIGHT, index.get())
104+
assertEquals(DEFAULT_BITMAP_WIDTH * DEFAULT_BITMAP_HEIGHT, analyzed.get())
100105
}
101106

102107
@Test
103-
fun oddNumberOfPixels() {
108+
fun `WHEN there are an odd number of pixels THEN analyze every pixel`() {
104109
val pixelProcessor = ParallelPixelProcessor
105110
.create(ParallelProcessorConfiguration(requestedNumberOfChunkThreads = 2))
106111
.baseline(mockBitmap(3, 3))
@@ -112,28 +117,43 @@ class ParallelPixelProcessorTest {
112117
0 to 2, 1 to 2, 2 to 2
113118
)
114119

115-
val index = AtomicInteger(0)
120+
val analyzed = AtomicInteger(0)
116121
pixelProcessor.analyze { _, _, (x, y) ->
117122
assertTrue(expected.remove(x to y))
118-
index.incrementAndGet()
123+
analyzed.incrementAndGet()
119124
true
120125
}
121-
assertEquals(9, index.get())
126+
assertEquals(9, analyzed.get())
122127
assertTrue(expected.isEmpty())
123128
}
124129

130+
/**
131+
* Assert that the position at [index] matches the expected [position]
132+
*/
125133
private fun ParallelPixelProcessor.assertPosition(index: Int, position: Pair<Int, Int>) {
126134
val (x, y) = this.getPosition(index, DEFAULT_BITMAP_WIDTH)
127135
assertEquals(position, x to y)
128136
}
129137

130138
@Test
131-
fun multicoreChunks() {
139+
fun `WHEN using multiple threads THEN the positions map correctly`() {
132140
setUp(maxNumberOfChunkThreads = 2).run {
133141
assertPosition(7, 7 to 0)
134142
assertPosition(500, 500 to 0)
135143
assertPosition(1500, 420 to 1)
136144
assertPosition(2200, 40 to 2)
137145
}
138146
}
147+
148+
@Test
149+
fun `WHEN a single pixel is different THEN fail early AND do not analyze every pixel`() {
150+
val pixelProcessor = setUp(maxNumberOfChunkThreads = 1)
151+
152+
val analyzed = AtomicInteger(0)
153+
pixelProcessor.analyze { _, _, (x, y) ->
154+
analyzed.incrementAndGet()
155+
(x != 1) || (y != 0)
156+
}
157+
assertEquals(2, analyzed.get())
158+
}
139159
}

0 commit comments

Comments
 (0)