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