1 /* 2 * Copyright 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 17 package androidx.camera.core.internal.utils; 18 19 import static androidx.core.util.Preconditions.checkArgument; 20 21 import static java.nio.ByteBuffer.allocateDirect; 22 23 import android.graphics.Bitmap; 24 import android.graphics.BitmapFactory; 25 import android.graphics.BitmapRegionDecoder; 26 import android.graphics.ImageFormat; 27 import android.graphics.Matrix; 28 import android.graphics.PixelFormat; 29 import android.graphics.Rect; 30 import android.graphics.RectF; 31 import android.graphics.YuvImage; 32 import android.util.Rational; 33 import android.util.Size; 34 35 import androidx.annotation.IntRange; 36 import androidx.camera.core.ImageProcessingUtil; 37 import androidx.camera.core.ImageProxy; 38 import androidx.camera.core.Logger; 39 import androidx.camera.core.impl.utils.ExifData; 40 import androidx.camera.core.impl.utils.ExifOutputStream; 41 42 import org.jspecify.annotations.NonNull; 43 import org.jspecify.annotations.Nullable; 44 45 import java.io.ByteArrayOutputStream; 46 import java.io.IOException; 47 import java.io.OutputStream; 48 import java.nio.ByteBuffer; 49 50 /** 51 * Utility class for image related operations. 52 */ 53 public final class ImageUtil { 54 private static final String TAG = "ImageUtil"; 55 56 /** 57 * Default RGBA pixel stride used by CameraX, with R, G, B and A each occupies 1 byte. 58 */ 59 public static final int DEFAULT_RGBA_PIXEL_STRIDE = 4; 60 ImageUtil()61 private ImageUtil() { 62 } 63 64 /** 65 * Creates {@link Bitmap} from {@link ImageProxy}. 66 * 67 * <p> Currently only {@link ImageFormat#YUV_420_888}, {@link ImageFormat#JPEG}, 68 * {@link ImageFormat#JPEG_R} and {@link PixelFormat#RGBA_8888} are supported. If the format 69 * is invalid, an {@link IllegalArgumentException} will be thrown. If the conversion to bimap 70 * failed, an {@link UnsupportedOperationException} will be thrown. 71 * 72 * @param imageProxy The input {@link ImageProxy} instance. 73 * @return {@link Bitmap} instance. 74 */ createBitmapFromImageProxy(@onNull ImageProxy imageProxy)75 public static @NonNull Bitmap createBitmapFromImageProxy(@NonNull ImageProxy imageProxy) { 76 switch (imageProxy.getFormat()) { 77 case ImageFormat.YUV_420_888: 78 return ImageProcessingUtil.convertYUVToBitmap(imageProxy); 79 case ImageFormat.JPEG: 80 case ImageFormat.JPEG_R: 81 return createBitmapFromJpegImage(imageProxy); 82 case PixelFormat.RGBA_8888: 83 return createBitmapFromRgbaImage(imageProxy); 84 default: 85 throw new IllegalArgumentException( 86 "Incorrect image format of the input image proxy: " 87 + imageProxy.getFormat() + ", only ImageFormat.YUV_420_888 and " 88 + "PixelFormat.RGBA_8888 are supported"); 89 } 90 } 91 92 /** 93 * Creates a {@link Bitmap} from an {@link ImageProxy.PlaneProxy} array. 94 * 95 * <p>This method expects a single plane with a pixel stride of 4 and a row stride of (width * 96 * 4). 97 */ createBitmapFromPlane( ImageProxy.PlaneProxy @onNull [] planes, int width, int height)98 public static @NonNull Bitmap createBitmapFromPlane( 99 ImageProxy.PlaneProxy @NonNull [] planes, int width, int height) { 100 checkArgument(planes.length == 1, "Expect a single plane"); 101 checkArgument(planes[0].getPixelStride() == DEFAULT_RGBA_PIXEL_STRIDE, 102 "Expect pixelStride=" + DEFAULT_RGBA_PIXEL_STRIDE); 103 checkArgument( 104 planes[0].getRowStride() == DEFAULT_RGBA_PIXEL_STRIDE * width, 105 "Expect rowStride=width*" + DEFAULT_RGBA_PIXEL_STRIDE); 106 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 107 // Rewind the buffer just to be safe. 108 planes[0].getBuffer().rewind(); 109 ImageProcessingUtil.copyByteBufferToBitmap(bitmap, planes[0].getBuffer(), 110 planes[0].getRowStride()); 111 return bitmap; 112 } 113 114 /** 115 * Rotates the bitmap by the given rotation degrees. 116 */ rotateBitmap(@onNull Bitmap bitmap, int rotationDegrees)117 public static @NonNull Bitmap rotateBitmap(@NonNull Bitmap bitmap, int rotationDegrees) { 118 Matrix matrix = new Matrix(); 119 matrix.postRotate(rotationDegrees); 120 return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, 121 true); 122 } 123 124 /** 125 * Creates a direct {@link ByteBuffer} and copy the content of the {@link Bitmap}. 126 */ createDirectByteBuffer(@onNull Bitmap bitmap)127 public static @NonNull ByteBuffer createDirectByteBuffer(@NonNull Bitmap bitmap) { 128 checkArgument(bitmap.getConfig() == Bitmap.Config.ARGB_8888, 129 "Only accept Bitmap with ARGB_8888 format for now."); 130 ByteBuffer byteBuffer = allocateDirect(bitmap.getAllocationByteCount()); 131 ImageProcessingUtil.copyBitmapToByteBuffer(bitmap, byteBuffer, bitmap.getRowBytes()); 132 byteBuffer.rewind(); 133 return byteBuffer; 134 } 135 136 /** 137 * Converts a {@link Size} to an float array of vertexes. 138 */ sizeToVertexes(@onNull Size size)139 public static float @NonNull [] sizeToVertexes(@NonNull Size size) { 140 return new float[]{0, 0, size.getWidth(), 0, size.getWidth(), size.getHeight(), 0, 141 size.getHeight()}; 142 } 143 144 /** 145 * Returns the min value. 146 */ min(float value1, float value2, float value3, float value4)147 public static float min(float value1, float value2, float value3, float value4) { 148 return Math.min(Math.min(value1, value2), Math.min(value3, value4)); 149 } 150 151 /** 152 * Rotates aspect ratio based on rotation degrees. 153 */ getRotatedAspectRatio( @ntRangefrom = 0, to = 359) int rotationDegrees, @NonNull Rational aspectRatio)154 public static @NonNull Rational getRotatedAspectRatio( 155 @IntRange(from = 0, to = 359) int rotationDegrees, 156 @NonNull Rational aspectRatio) { 157 if (rotationDegrees == 90 || rotationDegrees == 270) { 158 return inverseRational(aspectRatio); 159 } 160 161 return new Rational(aspectRatio.getNumerator(), aspectRatio.getDenominator()); 162 } 163 164 /** 165 * Converts JPEG or JPEG_R {@link ImageProxy} to JPEG byte array. 166 */ jpegImageToJpegByteArray(@onNull ImageProxy image)167 public static byte @NonNull [] jpegImageToJpegByteArray(@NonNull ImageProxy image) { 168 if (!isJpegFormats(image.getFormat())) { 169 throw new IllegalArgumentException( 170 "Incorrect image format of the input image proxy: " + image.getFormat()); 171 } 172 173 ImageProxy.PlaneProxy[] planes = image.getPlanes(); 174 ByteBuffer buffer = planes[0].getBuffer(); 175 byte[] data = new byte[buffer.capacity()]; 176 buffer.rewind(); 177 buffer.get(data); 178 179 return data; 180 } 181 182 /** 183 * Converts JPEG {@link ImageProxy} to JPEG byte array. The input JPEG image will be cropped 184 * by the specified crop rectangle and compressed by the specified quality value. 185 */ jpegImageToJpegByteArray(@onNull ImageProxy image, @NonNull Rect cropRect, @IntRange(from = 1, to = 100) int jpegQuality)186 public static byte @NonNull [] jpegImageToJpegByteArray(@NonNull ImageProxy image, 187 @NonNull Rect cropRect, @IntRange(from = 1, to = 100) int jpegQuality) 188 throws CodecFailedException { 189 if (!isJpegFormats(image.getFormat())) { 190 throw new IllegalArgumentException( 191 "Incorrect image format of the input image proxy: " + image.getFormat()); 192 } 193 194 byte[] data = jpegImageToJpegByteArray(image); 195 data = cropJpegByteArray(data, cropRect, jpegQuality); 196 197 return data; 198 } 199 200 /** 201 * Converts YUV_420_888 {@link ImageProxy} to JPEG byte array. The input YUV_420_888 image 202 * will be cropped if a non-null crop rectangle is specified. The output JPEG byte array will 203 * be compressed by the specified quality value. The rotationDegrees is set to the EXIF of 204 * the JPEG if it is not 0. 205 */ yuvImageToJpegByteArray(@onNull ImageProxy image, @Nullable Rect cropRect, @IntRange(from = 1, to = 100) int jpegQuality, int rotationDegrees)206 public static byte @NonNull [] yuvImageToJpegByteArray(@NonNull ImageProxy image, 207 @Nullable Rect cropRect, 208 @IntRange(from = 1, to = 100) 209 int jpegQuality, 210 int rotationDegrees) throws CodecFailedException { 211 if (image.getFormat() != ImageFormat.YUV_420_888) { 212 throw new IllegalArgumentException( 213 "Incorrect image format of the input image proxy: " + image.getFormat()); 214 } 215 216 byte[] yuvBytes = yuv_420_888toNv21(image); 217 YuvImage yuv = new YuvImage(yuvBytes, ImageFormat.NV21, image.getWidth(), image.getHeight(), 218 null); 219 220 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 221 OutputStream out = new ExifOutputStream( 222 byteArrayOutputStream, ExifData.create(image, rotationDegrees)); 223 if (cropRect == null) { 224 cropRect = new Rect(0, 0, image.getWidth(), image.getHeight()); 225 } 226 boolean success = 227 yuv.compressToJpeg(cropRect, jpegQuality, out); 228 if (!success) { 229 throw new CodecFailedException("YuvImage failed to encode jpeg.", 230 CodecFailedException.FailureType.ENCODE_FAILED); 231 } 232 return byteArrayOutputStream.toByteArray(); 233 } 234 235 /** {@link android.media.Image} to NV21 byte array. */ yuv_420_888toNv21(@onNull ImageProxy image)236 public static byte @NonNull [] yuv_420_888toNv21(@NonNull ImageProxy image) { 237 ImageProxy.PlaneProxy yPlane = image.getPlanes()[0]; 238 ImageProxy.PlaneProxy uPlane = image.getPlanes()[1]; 239 ImageProxy.PlaneProxy vPlane = image.getPlanes()[2]; 240 241 ByteBuffer yBuffer = yPlane.getBuffer(); 242 ByteBuffer uBuffer = uPlane.getBuffer(); 243 ByteBuffer vBuffer = vPlane.getBuffer(); 244 yBuffer.rewind(); 245 uBuffer.rewind(); 246 vBuffer.rewind(); 247 248 int ySize = yBuffer.remaining(); 249 250 int position = 0; 251 // TODO(b/115743986): Pull these bytes from a pool instead of allocating for every image. 252 byte[] nv21 = new byte[ySize + (image.getWidth() * image.getHeight() / 2)]; 253 254 // Add the full y buffer to the array. If rowStride > 1, some padding may be skipped. 255 for (int row = 0; row < image.getHeight(); row++) { 256 yBuffer.get(nv21, position, image.getWidth()); 257 position += image.getWidth(); 258 yBuffer.position( 259 Math.min(ySize, yBuffer.position() - image.getWidth() + yPlane.getRowStride())); 260 } 261 262 int chromaHeight = image.getHeight() / 2; 263 int chromaWidth = image.getWidth() / 2; 264 int vRowStride = vPlane.getRowStride(); 265 int uRowStride = uPlane.getRowStride(); 266 int vPixelStride = vPlane.getPixelStride(); 267 int uPixelStride = uPlane.getPixelStride(); 268 269 // Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to 270 // perform faster bulk gets from the byte buffers. 271 byte[] vLineBuffer = new byte[vRowStride]; 272 byte[] uLineBuffer = new byte[uRowStride]; 273 for (int row = 0; row < chromaHeight; row++) { 274 vBuffer.get(vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining())); 275 uBuffer.get(uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining())); 276 int vLineBufferPosition = 0; 277 int uLineBufferPosition = 0; 278 for (int col = 0; col < chromaWidth; col++) { 279 nv21[position++] = vLineBuffer[vLineBufferPosition]; 280 nv21[position++] = uLineBuffer[uLineBufferPosition]; 281 vLineBufferPosition += vPixelStride; 282 uLineBufferPosition += uPixelStride; 283 } 284 } 285 286 return nv21; 287 } 288 289 /** Crops JPEG or JPEG_R byte array with given {@link android.graphics.Rect}. */ 290 @SuppressWarnings("deprecation") cropJpegByteArray(byte @NonNull [] data, @NonNull Rect cropRect, @IntRange(from = 1, to = 100) int jpegQuality)291 private static byte @NonNull [] cropJpegByteArray(byte @NonNull [] data, @NonNull Rect cropRect, 292 @IntRange(from = 1, to = 100) int jpegQuality) throws CodecFailedException { 293 Bitmap bitmap; 294 try { 295 BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(data, 0, data.length, 296 false); 297 bitmap = decoder.decodeRegion(cropRect, new BitmapFactory.Options()); 298 decoder.recycle(); 299 } catch (IllegalArgumentException e) { 300 throw new CodecFailedException("Decode byte array failed with illegal argument." + e, 301 CodecFailedException.FailureType.DECODE_FAILED); 302 } catch (IOException e) { 303 throw new CodecFailedException("Decode byte array failed.", 304 CodecFailedException.FailureType.DECODE_FAILED); 305 } 306 307 if (bitmap == null) { 308 throw new CodecFailedException("Decode byte array failed.", 309 CodecFailedException.FailureType.DECODE_FAILED); 310 } 311 312 ByteArrayOutputStream out = new ByteArrayOutputStream(); 313 boolean success = bitmap.compress(Bitmap.CompressFormat.JPEG, jpegQuality, out); 314 if (!success) { 315 throw new CodecFailedException("Encode bitmap failed.", 316 CodecFailedException.FailureType.ENCODE_FAILED); 317 } 318 bitmap.recycle(); 319 320 return out.toByteArray(); 321 } 322 323 /** True if the given aspect ratio is meaningful. */ isAspectRatioValid(@ullable Rational aspectRatio)324 public static boolean isAspectRatioValid(@Nullable Rational aspectRatio) { 325 return aspectRatio != null && aspectRatio.floatValue() > 0 && !aspectRatio.isNaN(); 326 } 327 328 /** True if the given image format is JPEG or JPEG/R. */ isJpegFormats(int imageFormat)329 public static boolean isJpegFormats(int imageFormat) { 330 return imageFormat == ImageFormat.JPEG || imageFormat == ImageFormat.JPEG_R; 331 } 332 333 /** True if the given image format is RAW_SENSOR. */ isRawFormats(int imageFormat)334 public static boolean isRawFormats(int imageFormat) { 335 return imageFormat == ImageFormat.RAW_SENSOR; 336 } 337 338 /** True if the given aspect ratio is meaningful and has effect on the given size. */ isAspectRatioValid(@onNull Size sourceSize, @Nullable Rational aspectRatio)339 public static boolean isAspectRatioValid(@NonNull Size sourceSize, 340 @Nullable Rational aspectRatio) { 341 return aspectRatio != null 342 && aspectRatio.floatValue() > 0 343 && isCropAspectRatioHasEffect(sourceSize, aspectRatio) 344 && !aspectRatio.isNaN(); 345 } 346 347 /** 348 * Calculates crop rect with the specified aspect ratio on the given size. Assuming the rect is 349 * at the center of the source. 350 */ computeCropRectFromAspectRatio(@onNull Size sourceSize, @NonNull Rational aspectRatio)351 public static @Nullable Rect computeCropRectFromAspectRatio(@NonNull Size sourceSize, 352 @NonNull Rational aspectRatio) { 353 if (!isAspectRatioValid(aspectRatio)) { 354 Logger.w(TAG, "Invalid view ratio."); 355 return null; 356 } 357 358 int sourceWidth = sourceSize.getWidth(); 359 int sourceHeight = sourceSize.getHeight(); 360 float srcRatio = sourceWidth / (float) sourceHeight; 361 int cropLeft = 0; 362 int cropTop = 0; 363 int outputWidth = sourceWidth; 364 int outputHeight = sourceHeight; 365 int numerator = aspectRatio.getNumerator(); 366 int denominator = aspectRatio.getDenominator(); 367 368 if (aspectRatio.floatValue() > srcRatio) { 369 outputHeight = Math.round((sourceWidth / (float) numerator) * denominator); 370 cropTop = (sourceHeight - outputHeight) / 2; 371 } else { 372 outputWidth = Math.round((sourceHeight / (float) denominator) * numerator); 373 cropLeft = (sourceWidth - outputWidth) / 2; 374 } 375 376 return new Rect(cropLeft, cropTop, cropLeft + outputWidth, cropTop + outputHeight); 377 } 378 379 /** 380 * Calculates crop rect based on the dispatch resolution and rotation degrees info. 381 * 382 * <p> The original crop rect is calculated based on camera sensor buffer. On some devices, 383 * the buffer is rotated before being passed to users, in which case the crop rect also 384 * needs additional transformations. 385 * 386 * <p> There are two most common scenarios: 1) exif rotation is 0, or 2) exif rotation 387 * equals output rotation. 1) means the HAL rotated the buffer based on target 388 * rotation. 2) means HAL no-oped on the rotation. Theoretically only 1) needs 389 * additional transformations, but this method is also generic enough to handle all possible 390 * HAL rotations. 391 */ computeCropRectFromDispatchInfo(@onNull Rect surfaceCropRect, int surfaceToOutputDegrees, @NonNull Size dispatchResolution, int dispatchToOutputDegrees)392 public static @NonNull Rect computeCropRectFromDispatchInfo(@NonNull Rect surfaceCropRect, 393 int surfaceToOutputDegrees, @NonNull Size dispatchResolution, 394 int dispatchToOutputDegrees) { 395 // There are 3 coordinate systems: surface, dispatch and output. Surface is where 396 // the original crop rect is defined. We need to figure out what HAL 397 // has done to the buffer (the surface->dispatch mapping) and apply the same 398 // transformation to the crop rect. 399 // The surface->dispatch mapping is calculated by inverting a dispatch->surface mapping. 400 401 Matrix matrix = new Matrix(); 402 // Apply the dispatch->surface rotation. 403 matrix.setRotate(dispatchToOutputDegrees - surfaceToOutputDegrees); 404 // Apply the dispatch->surface translation. The translation is calculated by 405 // compensating for the offset caused by the dispatch->surface rotation. 406 float[] vertexes = sizeToVertexes(dispatchResolution); 407 matrix.mapPoints(vertexes); 408 float left = min(vertexes[0], vertexes[2], vertexes[4], vertexes[6]); 409 float top = min(vertexes[1], vertexes[3], vertexes[5], vertexes[7]); 410 matrix.postTranslate(-left, -top); 411 // Inverting the dispatch->surface mapping to get the surface->dispatch mapping. 412 matrix.invert(matrix); 413 414 // Apply the surface->dispatch mapping to surface crop rect. 415 RectF dispatchCropRectF = new RectF(); 416 matrix.mapRect(dispatchCropRectF, new RectF(surfaceCropRect)); 417 dispatchCropRectF.sort(); 418 Rect dispatchCropRect = new Rect(); 419 dispatchCropRectF.round(dispatchCropRect); 420 return dispatchCropRect; 421 } 422 isCropAspectRatioHasEffect(@onNull Size sourceSize, @NonNull Rational aspectRatio)423 private static boolean isCropAspectRatioHasEffect(@NonNull Size sourceSize, 424 @NonNull Rational aspectRatio) { 425 int sourceWidth = sourceSize.getWidth(); 426 int sourceHeight = sourceSize.getHeight(); 427 int numerator = aspectRatio.getNumerator(); 428 int denominator = aspectRatio.getDenominator(); 429 430 return sourceHeight != Math.round((sourceWidth / (float) numerator) * denominator) 431 || sourceWidth != Math.round((sourceHeight / (float) denominator) * numerator); 432 } 433 inverseRational(@ullable Rational rational)434 private static Rational inverseRational(@Nullable Rational rational) { 435 if (rational == null) { 436 return rational; 437 } 438 return new Rational( 439 /*numerator=*/ rational.getDenominator(), 440 /*denominator=*/ rational.getNumerator()); 441 } 442 createBitmapFromRgbaImage(@onNull ImageProxy imageProxy)443 private static @NonNull Bitmap createBitmapFromRgbaImage(@NonNull ImageProxy imageProxy) { 444 Bitmap bitmap = 445 Bitmap.createBitmap(imageProxy.getWidth(), 446 imageProxy.getHeight(), 447 Bitmap.Config.ARGB_8888); 448 // Rewind the buffer just to be safe. 449 imageProxy.getPlanes()[0].getBuffer().rewind(); 450 ImageProcessingUtil.copyByteBufferToBitmap(bitmap, imageProxy.getPlanes()[0].getBuffer(), 451 imageProxy.getPlanes()[0].getRowStride()); 452 return bitmap; 453 } 454 createBitmapFromJpegImage(@onNull ImageProxy imageProxy)455 private static @NonNull Bitmap createBitmapFromJpegImage(@NonNull ImageProxy imageProxy) { 456 byte[] bytes = jpegImageToJpegByteArray(imageProxy); 457 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null); 458 if (bitmap == null) { 459 throw new UnsupportedOperationException("Decode jpeg byte array failed"); 460 } 461 return bitmap; 462 } 463 464 /** 465 * Checks whether the image's crop rectangle is the same as the source image size. 466 */ shouldCropImage(@onNull ImageProxy image)467 public static boolean shouldCropImage(@NonNull ImageProxy image) { 468 return shouldCropImage(image.getWidth(), image.getHeight(), image.getCropRect().width(), 469 image.getCropRect().height()); 470 } 471 472 /** 473 * Checks whether the image's crop rectangle is the same as the source image size. 474 */ shouldCropImage(int sourceWidth, int sourceHeight, int cropRectWidth, int cropRectHeight)475 public static boolean shouldCropImage(int sourceWidth, int sourceHeight, int cropRectWidth, 476 int cropRectHeight) { 477 return sourceWidth != cropRectWidth || sourceHeight != cropRectHeight; 478 } 479 480 /** Exception for error during transcoding image. */ 481 public static final class CodecFailedException extends Exception { 482 public enum FailureType { 483 ENCODE_FAILED, 484 DECODE_FAILED, 485 UNKNOWN 486 } 487 488 private final FailureType mFailureType; 489 CodecFailedException(@onNull String message)490 CodecFailedException(@NonNull String message) { 491 super(message); 492 mFailureType = FailureType.UNKNOWN; 493 } 494 CodecFailedException(@onNull String message, @NonNull FailureType failureType)495 CodecFailedException(@NonNull String message, @NonNull FailureType failureType) { 496 super(message); 497 mFailureType = failureType; 498 } 499 getFailureType()500 public @NonNull FailureType getFailureType() { 501 return mFailureType; 502 } 503 } 504 } 505