1 /*
2  * Copyright 2023 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 androidx.camera.testing.impl;
18 
19 import static android.graphics.BitmapFactory.decodeByteArray;
20 import static android.graphics.ImageFormat.JPEG;
21 import static android.graphics.ImageFormat.JPEG_R;
22 import static android.graphics.ImageFormat.RAW_SENSOR;
23 import static android.graphics.ImageFormat.YUV_420_888;
24 
25 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA;
26 import static androidx.camera.testing.impl.ImageProxyUtil.YUV_FORMAT_PLANE_DATA_TYPE_I420;
27 import static androidx.camera.testing.impl.ImageProxyUtil.createRawImagePlanes;
28 import static androidx.camera.testing.impl.ImageProxyUtil.createYUV420ImagePlanes;
29 import static androidx.camera.testing.impl.ImageProxyUtil.getDefaultYuvFormatPlaneDataType;
30 import static androidx.core.util.Preconditions.checkState;
31 
32 import android.graphics.Bitmap;
33 import android.graphics.Canvas;
34 import android.graphics.Color;
35 import android.graphics.Gainmap;
36 import android.graphics.Matrix;
37 import android.graphics.Paint;
38 import android.graphics.Rect;
39 import android.os.Build;
40 
41 import androidx.annotation.RequiresApi;
42 import androidx.annotation.VisibleForTesting;
43 import androidx.camera.core.ImageInfo;
44 import androidx.camera.core.ImageProxy;
45 import androidx.camera.core.internal.CameraCaptureResultImageInfo;
46 import androidx.camera.testing.fakes.FakeCameraCaptureResult;
47 import androidx.camera.testing.impl.fakes.FakeImageProxy;
48 import androidx.camera.testing.impl.fakes.FakeJpegPlaneProxy;
49 
50 import org.jspecify.annotations.NonNull;
51 
52 import java.io.ByteArrayOutputStream;
53 import java.nio.ByteBuffer;
54 
55 /**
56  * Generates images for testing.
57  *
58  * <p> The images generated by this class contains 4 color blocks follows the pattern below. Each
59  * block have the same size and covers 1/4 of the image.
60  *
61  * <pre>
62  * ------------------
63  * |   red  | green |
64  * ------------------
65  * |  blue | yellow |
66  * ------------------
67  * </pre>
68  *
69  * <p> The gain map generated by this class contains 4 gray scale blocks follows the pattern below.
70  * Each block have the same size and covers 1/4 of the image.
71  *
72  * <pre>
73  * ------------------
74  * |      black     |
75  * ------------------
76  * |    dark gray   |
77  * ------------------
78  * |      gray      |
79  * ------------------
80  * |      white     |
81  * ------------------
82  * </pre>
83  */
84 public class TestImageUtil {
85 
86     @VisibleForTesting
87     public static final int COLOR_BLACK = 0xFF000000;
88     @VisibleForTesting
89     public static final int COLOR_DARK_GRAY = 0xFF404040;
90     @VisibleForTesting
91     public static final int COLOR_GRAY = 0xFF808080;
92     @VisibleForTesting
93     public static final int COLOR_WHITE = 0xFFFFFFFF;
94 
TestImageUtil()95     private TestImageUtil() {
96     }
97 
98     /**
99      * Creates a [FakeImageProxy] with YUV format.
100      */
createYuvFakeImageProxy(@onNull ImageInfo imageInfo, int width, int height)101     public static @NonNull FakeImageProxy createYuvFakeImageProxy(@NonNull ImageInfo imageInfo,
102             int width, int height) {
103         return createYuvFakeImageProxy(imageInfo, width, height, getDefaultYuvFormatPlaneDataType(
104                 RESOLUTION_VGA.getWidth(), RESOLUTION_VGA.getHeight()), false);
105     }
106 
107     /**
108      * Creates a [FakeImageProxy] with YUV format with the content of the image to match the
109      * value of {@link #createBitmap}.
110      */
createYuvFakeImageProxy(@onNull ImageInfo imageInfo, int width, int height, @ImageProxyUtil.YuvFormatPlaneDataType int yuvFormatPlaneDataType, boolean insertRgbTestData)111     public static @NonNull FakeImageProxy createYuvFakeImageProxy(@NonNull ImageInfo imageInfo,
112             int width, int height,
113             @ImageProxyUtil.YuvFormatPlaneDataType int yuvFormatPlaneDataType,
114             boolean insertRgbTestData) {
115         return createYuvFakeImageProxy(
116                 createYUV420ImagePlanes(width, height, yuvFormatPlaneDataType, false),
117                 imageInfo, width, height, 1,
118                 yuvFormatPlaneDataType == YUV_FORMAT_PLANE_DATA_TYPE_I420 ? 1 : 2,
119                 insertRgbTestData);
120     }
121 
122     /**
123      * Creates a [FakeImageProxy] with YUV format with the content of the image to match the
124      * value of {@link #createBitmap}.
125      */
createYuvFakeImageProxy(@onNull ImageInfo imageInfo, int width, int height, int pixelStrideY, int pixelStrideUV, boolean flipUV, boolean insertRgbTestData)126     public static @NonNull FakeImageProxy createYuvFakeImageProxy(@NonNull ImageInfo imageInfo,
127             int width, int height, int pixelStrideY, int pixelStrideUV, boolean flipUV,
128             boolean insertRgbTestData) {
129         return createYuvFakeImageProxy(
130                 createYUV420ImagePlanes(width, height, pixelStrideY, pixelStrideUV, flipUV, false),
131                 imageInfo, width, height, pixelStrideY, pixelStrideUV, insertRgbTestData);
132     }
133 
createYuvFakeImageProxy( ImageProxy.@onNull PlaneProxy[] planeProxies, @NonNull ImageInfo imageInfo, int width, int height, int pixelStrideY, int pixelStrideUV, boolean insertRgbTestData)134     private static @NonNull FakeImageProxy createYuvFakeImageProxy(
135             ImageProxy.@NonNull PlaneProxy[] planeProxies, @NonNull ImageInfo imageInfo, int width,
136             int height, int pixelStrideY, int pixelStrideUV, boolean insertRgbTestData) {
137         FakeImageProxy image = new FakeImageProxy(imageInfo);
138         image.setFormat(YUV_420_888);
139         image.setPlanes(planeProxies);
140         image.setWidth(width);
141         image.setHeight(height);
142 
143         // Directly returns the image if RGB test data is not needed.
144         if (!insertRgbTestData) {
145             return image;
146         }
147 
148         Bitmap rgbBitmap = createBitmap(width, height);
149         writeBitmapToYuvByteBuffers(rgbBitmap,
150                 image.getPlanes()[0].getBuffer(),
151                 image.getPlanes()[1].getBuffer(),
152                 image.getPlanes()[2].getBuffer(),
153                 pixelStrideY, pixelStrideUV);
154 
155         return image;
156     }
157 
writeBitmapToYuvByteBuffers( @onNull Bitmap bitmap, @NonNull ByteBuffer yByteBuffer, @NonNull ByteBuffer uByteBuffer, @NonNull ByteBuffer vByteBuffer, int pixelStrideY, int pixelStrideUV)158     private static void writeBitmapToYuvByteBuffers(
159             @NonNull Bitmap bitmap,
160             @NonNull ByteBuffer yByteBuffer,
161             @NonNull ByteBuffer uByteBuffer,
162             @NonNull ByteBuffer vByteBuffer,
163             int pixelStrideY,
164             int pixelStrideUV) {
165         int width = bitmap.getWidth();
166         int height = bitmap.getHeight();
167         int[] argb = new int[width * height];
168         bitmap.getPixels(argb, 0, width, 0, 0, width, height);
169 
170         int yIndex = 0;
171         int uvIndex = 0;
172 
173         for (int j = 0; j < height; j++) {
174             for (int i = 0; i < width; i++) {
175                 int rgb = argb[j * width + i];
176                 int r = (rgb >> 16) & 0xFF;
177                 int g = (rgb >> 8) & 0xFF;
178                 int b = rgb & 0xFF;
179 
180                 int y = (int) (0.299 * r + 0.587 * g + 0.114 * b);
181                 int u = (int) (-0.169 * r - 0.331 * g + 0.5 * b + 128);
182                 int v = (int) (0.5 * r - 0.419 * g - 0.081 * b + 128);
183 
184                 yByteBuffer.put(yIndex, (byte) y);
185                 yIndex += pixelStrideY;
186                 if (j % 2 == 0 && i % 2 == 0) {
187                     uByteBuffer.put(uvIndex * pixelStrideUV, (byte) u);
188                     vByteBuffer.put(uvIndex * pixelStrideUV, (byte) v);
189                     uvIndex++;
190                 }
191             }
192         }
193         yByteBuffer.rewind();
194         uByteBuffer.rewind();
195         vByteBuffer.rewind();
196     }
197 
198     /**
199      * Creates a [FakeImageProxy] with [RAW_SENSOR] format.
200      */
createRawFakeImageProxy(@onNull ImageInfo imageInfo, int width, int height)201     public static @NonNull FakeImageProxy createRawFakeImageProxy(@NonNull ImageInfo imageInfo,
202             int width, int height) {
203         FakeImageProxy image = new FakeImageProxy(imageInfo);
204         image.setFormat(RAW_SENSOR);
205         image.setPlanes(createRawImagePlanes(width, height, 2, false));
206         image.setWidth(width);
207         image.setHeight(height);
208         return image;
209     }
210 
211     /**
212      * Creates a {@link FakeImageProxy} from JPEG bytes.
213      */
createJpegFakeImageProxy(@onNull ImageInfo imageInfo, byte @NonNull [] jpegBytes)214     public static @NonNull FakeImageProxy createJpegFakeImageProxy(@NonNull ImageInfo imageInfo,
215             byte @NonNull [] jpegBytes) {
216         Bitmap bitmap = decodeByteArray(jpegBytes, 0, jpegBytes.length);
217         return createJpegFakeImageProxy(imageInfo, jpegBytes, bitmap.getWidth(),
218                 bitmap.getHeight());
219     }
220 
221     /**
222      * Creates a {@link FakeImageProxy} from JPEG bytes of JPEG/R.
223      */
createJpegrFakeImageProxy(@onNull ImageInfo imageInfo, byte @NonNull [] jpegBytes)224     public static @NonNull FakeImageProxy createJpegrFakeImageProxy(@NonNull ImageInfo imageInfo,
225             byte @NonNull [] jpegBytes) {
226         Bitmap bitmap = decodeByteArray(jpegBytes, 0, jpegBytes.length);
227         return createJpegrFakeImageProxy(imageInfo, jpegBytes, bitmap.getWidth(),
228                 bitmap.getHeight());
229     }
230 
231     /**
232      * Creates a {@link FakeImageProxy} from JPEG bytes.
233      */
createJpegFakeImageProxy(@onNull ImageInfo imageInfo, byte @NonNull [] jpegBytes, int width, int height)234     public static @NonNull FakeImageProxy createJpegFakeImageProxy(@NonNull ImageInfo imageInfo,
235             byte @NonNull [] jpegBytes, int width, int height) {
236         FakeImageProxy image = new FakeImageProxy(imageInfo);
237         image.setFormat(JPEG);
238         image.setPlanes(new FakeJpegPlaneProxy[]{new FakeJpegPlaneProxy(jpegBytes)});
239         image.setWidth(width);
240         image.setHeight(height);
241         return image;
242     }
243 
244     /**
245      * Creates a {@link FakeImageProxy} from JPEG bytes of JPEG/R.
246      */
createJpegrFakeImageProxy(@onNull ImageInfo imageInfo, byte @NonNull [] jpegBytes, int width, int height)247     public static @NonNull FakeImageProxy createJpegrFakeImageProxy(@NonNull ImageInfo imageInfo,
248             byte @NonNull [] jpegBytes, int width, int height) {
249         FakeImageProxy image = new FakeImageProxy(imageInfo);
250         image.setFormat(JPEG_R);
251         image.setPlanes(new FakeJpegPlaneProxy[]{new FakeJpegPlaneProxy(jpegBytes)});
252         image.setWidth(width);
253         image.setHeight(height);
254         return image;
255     }
256 
257     /**
258      * Creates a {@link FakeImageProxy} from JPEG bytes.
259      */
createJpegFakeImageProxy(byte @NonNull [] jpegBytes)260     public static @NonNull FakeImageProxy createJpegFakeImageProxy(byte @NonNull [] jpegBytes) {
261         return createJpegFakeImageProxy(
262                 new CameraCaptureResultImageInfo(new FakeCameraCaptureResult()), jpegBytes);
263     }
264 
265     /**
266      * Creates a {@link FakeImageProxy} from JPEG bytes of JPEG/R.
267      */
createJpegrFakeImageProxy(byte @NonNull [] jpegBytes)268     public static @NonNull FakeImageProxy createJpegrFakeImageProxy(byte @NonNull [] jpegBytes) {
269         return createJpegrFakeImageProxy(
270                 new CameraCaptureResultImageInfo(new FakeCameraCaptureResult()), jpegBytes);
271     }
272 
273     /**
274      * Generates a JPEG image.
275      */
createJpegBytes(int width, int height)276     public static byte @NonNull [] createJpegBytes(int width, int height) {
277         Bitmap bitmap = createBitmap(width, height);
278         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
279         bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
280         return outputStream.toByteArray();
281     }
282 
283     /**
284      * Generates a JPEG/R image.
285      */
286     @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
createJpegrBytes(int width, int height)287     public static byte @NonNull [] createJpegrBytes(int width, int height) {
288         Bitmap bitmap = createBitmapWithGainmap(width, height);
289         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
290         bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
291         return outputStream.toByteArray();
292     }
293 
294     /**
295      * Generates a A24 problematic JPEG image.
296      */
createA24ProblematicJpegByteArray(int width, int height)297     public static byte @NonNull [] createA24ProblematicJpegByteArray(int width, int height) {
298         byte[] incorrectHeaderByteData =
299                 new byte[]{(byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe1, (byte) 0xff,
300                         (byte) 0x7c, (byte) 0x45, (byte) 0x78, (byte) 0x69, (byte) 0x66,
301                         (byte) 0x00, (byte) 0x00};
302         byte[] jpegBytes = createJpegBytes(width, height);
303         byte[] result = new byte[incorrectHeaderByteData.length + jpegBytes.length];
304         System.arraycopy(incorrectHeaderByteData, 0, result, 0, incorrectHeaderByteData.length);
305         System.arraycopy(jpegBytes, 0, result, incorrectHeaderByteData.length, jpegBytes.length);
306         return result;
307     }
308 
309     /**
310      * Generates a {@link Bitmap} image and paints it with 4 color blocks.
311      */
createBitmap(int width, int height)312     public static @NonNull Bitmap createBitmap(int width, int height) {
313         Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
314         int centerX = width / 2;
315         int centerY = height / 2;
316         Canvas canvas = new Canvas(bitmap);
317         canvas.drawRect(0, 0, centerX, centerY, createPaint(Color.RED));
318         canvas.drawRect(centerX, 0, width, centerY, createPaint(Color.GREEN));
319         canvas.drawRect(centerX, centerY, width, height, createPaint(Color.YELLOW));
320         canvas.drawRect(0, centerY, centerX, height, createPaint(Color.BLUE));
321         return bitmap;
322     }
323 
324     /**
325      * Generates a {@link Bitmap} image (contains 4 color blocks) with gain map (contains 4 gray
326      * scale blocks) set.
327      */
328     @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
createBitmapWithGainmap(int width, int height)329     public static @NonNull Bitmap createBitmapWithGainmap(int width, int height) {
330         Bitmap bitmap = createBitmap(width, height);
331         Api34Impl.setGainmap(bitmap, createGainmap(width, height));
332         return bitmap;
333     }
334 
335     /**
336      * Generates a {@link Gainmap} image and paints it with 4 gray scale blocks.
337      */
338     @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
createGainmap(int width, int height)339     public static @NonNull Gainmap createGainmap(int width, int height) {
340         Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
341         int oneFourthY = height / 4;
342         int twoFourthsY = oneFourthY * 2;
343         int threeFourthsY = oneFourthY * 3;
344         Canvas canvas = new Canvas(bitmap);
345         canvas.drawRect(0, 0, width, oneFourthY, createPaint(COLOR_BLACK));
346         canvas.drawRect(0, oneFourthY, width, twoFourthsY, createPaint(COLOR_DARK_GRAY));
347         canvas.drawRect(0, twoFourthsY, width, threeFourthsY, createPaint(COLOR_GRAY));
348         canvas.drawRect(0, threeFourthsY, width, height, createPaint(COLOR_WHITE));
349         return Api34Impl.createGainmap(bitmap);
350     }
351 
352     /**
353      * Rotates the bitmap clockwise by the given degrees.
354      */
rotateBitmap(@onNull Bitmap bitmap, int rotationDegrees)355     public static @NonNull Bitmap rotateBitmap(@NonNull Bitmap bitmap, int rotationDegrees) {
356         Matrix matrix = new Matrix();
357         matrix.postRotate(rotationDegrees);
358         return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix,
359                 true);
360     }
361 
362     /**
363      * Calculates the average color difference between the 2 JPEG images.
364      */
getAverageDiff(byte @NonNull [] jpeg1, byte @NonNull [] jpeg2)365     public static int getAverageDiff(byte @NonNull [] jpeg1, byte @NonNull [] jpeg2) {
366         return getAverageDiff(
367                 decodeByteArray(jpeg1, 0, jpeg1.length),
368                 decodeByteArray(jpeg2, 0, jpeg2.length));
369     }
370 
371     /**
372      * Calculates the average color difference between the 2 bitmaps.
373      */
getAverageDiff(@onNull Bitmap bitmap1, @NonNull Bitmap bitmap2)374     public static int getAverageDiff(@NonNull Bitmap bitmap1, @NonNull Bitmap bitmap2) {
375         checkState(bitmap1.getWidth() == bitmap2.getWidth());
376         checkState(bitmap1.getHeight() == bitmap2.getHeight());
377         int totalDiff = 0;
378         for (int i = 0; i < bitmap1.getWidth(); i++) {
379             for (int j = 0; j < bitmap1.getHeight(); j++) {
380                 totalDiff += calculateColorDiff(bitmap1.getPixel(i, j), bitmap2.getPixel(i, j));
381             }
382         }
383         return totalDiff / (bitmap1.getWidth() * bitmap2.getHeight());
384     }
385 
386     /**
387      * Calculates the average color difference, between the given image/crop rect and the color.
388      *
389      * <p>This method is used for checking the content of an image is correct.
390      */
getAverageDiff(@onNull Bitmap bitmap, @NonNull Rect rect, int color)391     public static int getAverageDiff(@NonNull Bitmap bitmap, @NonNull Rect rect, int color) {
392         int totalDiff = 0;
393         for (int i = rect.left; i < rect.right; i++) {
394             for (int j = rect.top; j < rect.bottom; j++) {
395                 totalDiff += calculateColorDiff(bitmap.getPixel(i, j), color);
396             }
397         }
398         return totalDiff / (rect.width() * rect.height());
399     }
400 
401     /**
402      * Calculates the difference between 2 colors.
403      *
404      * <p>The difference is calculated as the average difference of each R, G and B color
405      * components.
406      */
calculateColorDiff(int color1, int color2)407     private static int calculateColorDiff(int color1, int color2) {
408         int diff = 0;
409         for (int shift = 0; shift <= 16; shift += 8) {
410             diff += Math.abs(((color1 >> shift) & 0xFF) - ((color2 >> shift) & 0xFF));
411         }
412         return diff / 3;
413     }
414 
415     /**
416      * Creates a FILL paint with the given color.
417      */
createPaint(int color)418     private static Paint createPaint(int color) {
419         Paint paint = new Paint();
420         paint.setStyle(Paint.Style.FILL);
421         paint.setColor(color);
422         return paint;
423     }
424 
425     @RequiresApi(34)
426     private static class Api34Impl {
createGainmap(@onNull Bitmap bitmap)427         static Gainmap createGainmap(@NonNull Bitmap bitmap) {
428             return new Gainmap(bitmap);
429         }
430 
setGainmap(@onNull Bitmap bitmap, @NonNull Gainmap gainmap)431         static void setGainmap(@NonNull Bitmap bitmap, @NonNull Gainmap gainmap) {
432             bitmap.setGainmap(gainmap);
433         }
434 
435         // This class is not instantiable.
Api34Impl()436         private Api34Impl() {
437         }
438     }
439 }
440