1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package platform.test.screenshot.matchers 17 18 import android.graphics.Bitmap 19 import android.graphics.Color 20 import android.graphics.Rect 21 import kotlin.math.abs 22 import kotlin.math.sqrt 23 import platform.test.screenshot.proto.ScreenshotResultProto 24 25 /** 26 * Matcher for differences not detectable by human eye 27 * The relaxed threshold allows for low quality png storage 28 * TODO(b/238758872): replace after b/238758872 is closed 29 */ 30 class AlmostPerfectMatcher( 31 private val acceptableThreshold: Double = 0.0, 32 ) : BitmapMatcher() { compareBitmapsnull33 override fun compareBitmaps( 34 expected: IntArray, 35 given: IntArray, 36 width: Int, 37 height: Int, 38 regions: List<Rect> 39 ): MatchResult { 40 check(expected.size == given.size) { "Size of two bitmaps does not match" } 41 42 val filter = getFilter(width, height, regions) 43 var different = 0 44 var same = 0 45 var ignored = 0 46 47 val diffArray = IntArray(width * height) 48 49 for (x in 0 until width) { 50 for (y in 0 until height) { 51 val index = x + y * width 52 if (filter[index] == 0) { 53 ignored++ 54 continue 55 } 56 val referenceColor = expected[index] 57 val testColor = given[index] 58 if (areSame(referenceColor, testColor)) { 59 ++same 60 } else { 61 ++different 62 } 63 diffArray[index] = 64 diffColor( 65 referenceColor, 66 testColor 67 ) 68 } 69 } 70 71 val stats = ScreenshotResultProto.DiffResult.ComparisonStatistics 72 .newBuilder() 73 .setNumberPixelsCompared(width * height) 74 .setNumberPixelsIdentical(same) 75 .setNumberPixelsDifferent(different) 76 .setNumberPixelsIgnored(ignored) 77 .build() 78 79 if (different > (acceptableThreshold * width * height)) { 80 val diff = Bitmap.createBitmap(diffArray, width, height, Bitmap.Config.ARGB_8888) 81 return MatchResult(matches = false, diff = diff, comparisonStatistics = stats) 82 } 83 return MatchResult(matches = true, diff = null, comparisonStatistics = stats) 84 } 85 diffColornull86 private fun diffColor(referenceColor: Int, testColor: Int): Int { 87 return if (areSame(referenceColor, testColor)) { 88 Color.TRANSPARENT 89 } else { 90 Color.MAGENTA 91 } 92 } 93 94 // ref 95 // R. F. Witzel, R. W. Burnham, and J. W. Onley. Threshold and suprathreshold perceptual color 96 // differences. J. Optical Society of America, 63:615{625, 1973. 14 areSamenull97 private fun areSame(referenceColor: Int, testColor: Int): Boolean { 98 val green = Color.green(referenceColor) - Color.green(testColor) 99 val blue = Color.blue(referenceColor) - Color.blue(testColor) 100 val red = Color.red(referenceColor) - Color.red(testColor) 101 val redDelta = abs(red) 102 val redScalar = if (redDelta < 128) 2 else 3 103 val blueScalar = if (redDelta < 128) 3 else 2 104 val greenScalar = 4 105 val correction = sqrt(( 106 (redScalar * red * red) + 107 (greenScalar * green * green) + 108 (blueScalar * blue * blue)) 109 .toDouble()) 110 // 1.5 no difference 111 // 3.0 observable by experienced human observer 112 // 6.0 minimal difference 113 // 12.0 perceivable difference 114 return correction <= 3.0 115 } 116 } 117