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