1 /*
2  * Copyright (C) 2014 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 
17 package android.theme.cts;
18 
19 import com.android.ddmlib.Log;
20 import com.android.ddmlib.Log.LogLevel;
21 import com.android.tradefed.util.Pair;
22 
23 import java.awt.Color;
24 import java.awt.image.BufferedImage;
25 import java.io.File;
26 import java.io.IOException;
27 import java.lang.String;
28 import java.util.concurrent.Callable;
29 
30 import javax.imageio.ImageIO;
31 
32 /**
33  * Compares the images generated by the device with the reference images.
34  */
35 public class ComparisonTask implements Callable<Pair<String, File>> {
36     private static final String TAG = "ComparisonTask";
37 
38     private static final int IMAGE_THRESHOLD = 8;
39 
40     /** Neutral gray for blending colors. */
41     private static final int GRAY = 0xFF808080;
42 
43     /** Maximum allowable number of consecutive failed pixels. */
44     private static final int MAX_CONSECUTIVE_FAILURES = 1;
45 
46     private final String mName;
47     private final File mExpected;
48     private final File mActual;
49 
ComparisonTask(String name, File expected, File actual)50     public ComparisonTask(String name, File expected, File actual) {
51         mName = name;
52         mExpected = expected;
53         mActual = actual;
54     }
55 
call()56     public Pair<String, File> call() {
57         try {
58             final BufferedImage expected = ImageIO.read(mExpected);
59             final BufferedImage actual = ImageIO.read(mActual);
60             if (!compare(expected, actual, IMAGE_THRESHOLD)) {
61                 final File diff = File.createTempFile("diff_" + mExpected.getName(), ".png");
62                 createDiff(expected, actual, diff);
63                 return new Pair<>(mName, diff);
64             }
65         } catch (IOException e) {
66             Log.logAndDisplay(LogLevel.ERROR, TAG, e.toString());
67             e.printStackTrace();
68         }
69 
70         return null;
71     }
72 
getAlphaScaledBlue(final int color)73     private static int getAlphaScaledBlue(final int color) {
74         return (color & 0x000000FF) * getAlpha(color) / 255;
75     }
76 
getAlphaScaledGreen(final int color)77     private static int getAlphaScaledGreen(final int color) {
78         return ((color & 0x0000FF00) >> 8) * getAlpha(color) / 255;
79     }
80 
getAlphaScaledRed(final int color)81     private static int getAlphaScaledRed(final int color) {
82         return ((color & 0x00FF0000) >> 16) * getAlpha(color) / 255;
83     }
84 
getAlpha(final int color)85     private static int getAlpha(final int color) {
86         // use logical shift for keeping an unsigned value
87         return (color & 0xFF000000) >>> 24;
88     }
89 
90 
91     /**
92      * Verifies that the pixels of reference and generated images are similar
93      * within a specified threshold.
94      *
95      * @param reference expected image
96      * @param generated actual image
97      * @param threshold maximum difference per channel
98      * @return {@code true} if the images are similar, false otherwise
99      */
compare(BufferedImage reference, BufferedImage generated, int threshold)100     private static boolean compare(BufferedImage reference, BufferedImage generated,
101             int threshold) {
102         final int w = generated.getWidth();
103         final int h = generated.getHeight();
104         if (w != reference.getWidth() || h != reference.getHeight()) {
105             return false;
106         }
107 
108         double maxDist = 0;
109         for (int i = 0; i < w; i++) {
110             int consecutive = 0;
111 
112             for (int j = 0; j < h; j++) {
113                 final int p1 = reference.getRGB(i, j);
114                 final int p2 = generated.getRGB(i, j);
115 
116                 final int dr = getAlphaScaledRed(p1) - getAlphaScaledRed(p2);
117                 final int dg = getAlphaScaledGreen(p1) - getAlphaScaledGreen(p2);
118                 final int db = getAlphaScaledBlue(p1) - getAlphaScaledBlue(p2);
119 
120                 if (Math.abs(db) > threshold ||
121                     Math.abs(dg) > threshold ||
122                     Math.abs(dr) > threshold) {
123                     System.err.println("fail dr=" + dr+ " dg=" + dg+ " db=" + db);
124 
125                     consecutive++;
126 
127                     if (consecutive > MAX_CONSECUTIVE_FAILURES) {
128                         System.err.println("consecutive fail");
129                         return false;
130                     }
131                 } else {
132                     consecutive = 0;
133                 }
134             }
135         }
136         return true;
137     }
138 
139     /**
140      * Returns the perceptual difference score (lower is better) for the
141      * provided ARGB pixels.
142      */
computeLabDistance(int p1, int p2)143     private static double computeLabDistance(int p1, int p2) {
144         // Blend with neutral gray to account for opacity.
145         p1 = ColorUtils.blendSrcOver(p1, GRAY);
146         p2 = ColorUtils.blendSrcOver(p2, GRAY);
147 
148         // Convert to LAB.
149         double[] lab1 = new double[3];
150         double[] lab2 = new double[3];
151         ColorUtils.colorToLAB(p1, lab1);
152         ColorUtils.colorToLAB(p2, lab2);
153 
154         // Compute the distance
155         double dist = 0;
156         for (int i = 0; i < 3; i++) {
157             double delta = lab1[i] - lab2[i];
158             dist += delta * delta;
159         }
160         return Math.sqrt(dist);
161     }
162 
createDiff(BufferedImage expected, BufferedImage actual, File out)163     private static void createDiff(BufferedImage expected, BufferedImage actual, File out)
164             throws IOException {
165         final int w1 = expected.getWidth();
166         final int h1 = expected.getHeight();
167         final int w2 = actual.getWidth();
168         final int h2 = actual.getHeight();
169         final int width = Math.max(w1, w2);
170         final int height = Math.max(h1, h2);
171 
172         // The diff will contain image1, image2 and the difference between the two.
173         final BufferedImage diff = new BufferedImage(
174                 width * 3, height, BufferedImage.TYPE_INT_ARGB);
175 
176         for (int i = 0; i < width; i++) {
177             for (int j = 0; j < height; j++) {
178                 final boolean inBounds1 = i < w1 && j < h1;
179                 final boolean inBounds2 = i < w2 && j < h2;
180                 int colorExpected = Color.WHITE.getRGB();
181                 int colorActual = Color.WHITE.getRGB();
182                 int colorDiff;
183                 if (inBounds1 && inBounds2) {
184                     colorExpected = expected.getRGB(i, j);
185                     colorActual = actual.getRGB(i, j);
186                     colorDiff = colorExpected == colorActual ? colorExpected : Color.RED.getRGB();
187                 } else if (inBounds1 && !inBounds2) {
188                     colorExpected = expected.getRGB(i, j);
189                     colorDiff = Color.BLUE.getRGB();
190                 } else if (!inBounds1 && inBounds2) {
191                     colorActual = actual.getRGB(i, j);
192                     colorDiff = Color.GREEN.getRGB();
193                 } else {
194                     colorDiff = Color.MAGENTA.getRGB();
195                 }
196 
197                 int x = i;
198                 diff.setRGB(x, j, colorExpected);
199                 x += width;
200                 diff.setRGB(x, j, colorActual);
201                 x += width;
202                 diff.setRGB(x, j, colorDiff);
203             }
204         }
205 
206         ImageIO.write(diff, "png", out);
207     }
208 
209 }
210