1 /*
2  * Copyright 2021 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.impl.utils;
18 
19 import android.graphics.Matrix;
20 import android.graphics.Rect;
21 import android.graphics.RectF;
22 import android.media.ExifInterface;
23 import android.util.Size;
24 import android.util.SizeF;
25 
26 import androidx.camera.core.internal.utils.ImageUtil;
27 import androidx.core.util.Preconditions;
28 
29 import org.jspecify.annotations.NonNull;
30 
31 import java.util.Locale;
32 
33 /**
34  * Utility class for transform.
35  *
36  * <p> The vertices representation uses a float array to represent a rectangle with arbitrary
37  * rotation and rotation-direction. It could be otherwise represented by a triple of a
38  * {@link RectF}, a rotation degrees integer and a boolean flag for the rotation-direction
39  * (clockwise v.s. counter-clockwise).
40  *
41  * TODO(b/179827713): merge this with {@link ImageUtil}.
42  */
43 public class TransformUtils {
44 
45     // Normalized space (-1, -1) - (1, 1).
46     public static final RectF NORMALIZED_RECT = new RectF(-1, -1, 1, 1);
47 
TransformUtils()48     private TransformUtils() {
49     }
50 
51     /**
52      * Gets the size of the {@link Rect}.
53      */
rectToSize(@onNull Rect rect)54     public static @NonNull Size rectToSize(@NonNull Rect rect) {
55         return new Size(rect.width(), rect.height());
56     }
57 
58     /** Returns a formatted string for a Rect. */
rectToString(@onNull Rect rect)59     public static @NonNull String rectToString(@NonNull Rect rect) {
60         return String.format(Locale.US, "%s(%dx%d)", rect, rect.width(), rect.height());
61     }
62 
63     /**
64      * Transforms size to a {@link Rect} with zero left and top.
65      */
sizeToRect(@onNull Size size)66     public static @NonNull Rect sizeToRect(@NonNull Size size) {
67         return sizeToRect(size, 0, 0);
68     }
69 
70     /**
71      * Transforms a size to a {@link Rect} with given left and top.
72      */
sizeToRect(@onNull Size size, int left, int top)73     public static @NonNull Rect sizeToRect(@NonNull Size size, int left, int top) {
74         return new Rect(left, top, left + size.getWidth(), top + size.getHeight());
75     }
76 
77     /**
78      * Returns true if the crop rect does not match the size.
79      */
hasCropping(@onNull Rect cropRect, @NonNull Size size)80     public static boolean hasCropping(@NonNull Rect cropRect, @NonNull Size size) {
81         return cropRect.left != 0 || cropRect.top != 0 || cropRect.width() != size.getWidth()
82                 || cropRect.height() != size.getHeight();
83     }
84 
85     /**
86      * Transforms size to a {@link RectF} with zero left and top.
87      */
sizeToRectF(@onNull Size size)88     public static @NonNull RectF sizeToRectF(@NonNull Size size) {
89         return sizeToRectF(size, 0, 0);
90     }
91 
92     /**
93      * Transforms a size to a {@link RectF} with given left and top.
94      */
sizeToRectF(@onNull Size size, int left, int top)95     public static @NonNull RectF sizeToRectF(@NonNull Size size, int left, int top) {
96         return new RectF(left, top, left + size.getWidth(), top + size.getHeight());
97     }
98 
99     /**
100      * Reverses width and height for a {@link Size}.
101      *
102      * @param size the size to reverse
103      * @return reversed size
104      */
reverseSize(@onNull Size size)105     public static @NonNull Size reverseSize(@NonNull Size size) {
106         return new Size(size.getHeight(), size.getWidth());
107     }
108 
109     /**
110      * Reverses width and height for a {@link SizeF}.
111      *
112      * @param sizeF the float size to reverse
113      * @return reversed float size
114      */
reverseSizeF(@onNull SizeF sizeF)115     public static @NonNull SizeF reverseSizeF(@NonNull SizeF sizeF) {
116         return new SizeF(sizeF.getHeight(), sizeF.getWidth());
117     }
118 
119     /**
120      * Rotates a {@link Size} according to the rotation degrees.
121      *
122      * @param size            the size to rotate
123      * @param rotationDegrees the rotation degrees
124      * @return rotated size
125      * @throws IllegalArgumentException if the rotation degrees is not a multiple of 90
126      */
rotateSize(@onNull Size size, int rotationDegrees)127     public static @NonNull Size rotateSize(@NonNull Size size, int rotationDegrees) {
128         Preconditions.checkArgument(rotationDegrees % 90 == 0,
129                 "Invalid rotation degrees: " + rotationDegrees);
130         return is90or270(within360(rotationDegrees)) ? reverseSize(size) : size;
131     }
132 
133     /**
134      * Rotates {@link SizeF} according to the rotation degrees.
135      *
136      * <p> A 640, 480 rect rotated 90 degrees clockwise will become a 480, 640 rect.
137      */
rotateRect(@onNull RectF rect, int rotationDegrees)138     public static @NonNull RectF rotateRect(@NonNull RectF rect, int rotationDegrees) {
139         Preconditions.checkArgument(rotationDegrees % 90 == 0,
140                 "Invalid rotation degrees: " + rotationDegrees);
141         if (is90or270(within360(rotationDegrees))) {
142             return new RectF(0, 0, /*right=*/rect.height(),  /*bottom=*/rect.width());
143         } else {
144             return rect;
145         }
146     }
147 
148     /**
149      * Checks if the matrix contains a mirroring.
150      *
151      * <p>This is mostly for testing if a sensor-to-buffer transformation. This method returns true
152      * if the image has been mirrored by the pipeline.
153      */
isMirrored(@onNull Matrix matrix)154     public static boolean isMirrored(@NonNull Matrix matrix) {
155         // We create 2 vectors, (0, 1) and (1, 0) with -90 degrees angle between them. Then we map
156         // the vectors with the matrix. If the angle changes to positive(90 degrees), we know that
157         // the matrix contains a mirroring.
158         float[] vectors = new float[]{0, 1, 1, 0};
159         matrix.mapVectors(vectors);
160         return calculateSignedAngle(vectors[0], vectors[1], vectors[2], vectors[3]) > 0;
161     }
162 
163     /**
164      * Calculates the clockwise angle between 2 vectors.
165      */
calculateSignedAngle(float v1x, float v1y, float v2x, float v2y)166     public static float calculateSignedAngle(float v1x, float v1y, float v2x, float v2y) {
167         // Calculate the dot product
168         float dotProduct = v1x * v2x + v1y * v2y;
169 
170         // Calculate the determinant (which is proportional to the sine of the angle)
171         float det = v1x * v2y - v1y * v2x;
172 
173         // Calculate the magnitudes of the vectors
174         double magV1 = Math.sqrt(v1x * v1x + v1y * v1y);
175         double magV2 = Math.sqrt(v2x * v2x + v2y * v2y);
176 
177         // Calculate the cosine and sine of the angle
178         double cosTheta = dotProduct / (magV1 * magV2);
179         double sinTheta = det / (magV1 * magV2);
180 
181         // Calculate the angle in radians using atan2 (result ranges from -π to π)
182         double angleRad = Math.atan2(sinTheta, cosTheta);
183 
184         // Convert the angle to degrees, if needed
185         double angleDeg = Math.toDegrees(angleRad);
186 
187         return (float) angleDeg;
188     }
189 
190     /**
191      * Gets the size after cropping and rotating.
192      *
193      * @return rotated size
194      * @throws IllegalArgumentException if the rotation degrees is not a multiple of.
195      */
getRotatedSize(@onNull Rect cropRect, int rotationDegrees)196     public static @NonNull Size getRotatedSize(@NonNull Rect cropRect, int rotationDegrees) {
197         return rotateSize(rectToSize(cropRect), rotationDegrees);
198     }
199 
200     /**
201      * Converts the degrees to within 360 degrees [0 - 359].
202      */
within360(int degrees)203     public static int within360(int degrees) {
204         return (degrees % 360 + 360) % 360;
205     }
206 
207     /**
208      * Converts an array of vertices to a {@link RectF}.
209      */
verticesToRect(float @NonNull [] vertices)210     public static @NonNull RectF verticesToRect(float @NonNull [] vertices) {
211         return new RectF(
212                 min(vertices[0], vertices[2], vertices[4], vertices[6]),
213                 min(vertices[1], vertices[3], vertices[5], vertices[7]),
214                 max(vertices[0], vertices[2], vertices[4], vertices[6]),
215                 max(vertices[1], vertices[3], vertices[5], vertices[7])
216         );
217     }
218 
219     /**
220      * Returns the max value.
221      */
max(float value1, float value2, float value3, float value4)222     public static float max(float value1, float value2, float value3, float value4) {
223         return Math.max(Math.max(value1, value2), Math.max(value3, value4));
224     }
225 
226     /**
227      * Returns the min value.
228      */
min(float value1, float value2, float value3, float value4)229     public static float min(float value1, float value2, float value3, float value4) {
230         return Math.min(Math.min(value1, value2), Math.min(value3, value4));
231     }
232 
233     /**
234      * Returns true if the rotation degrees is 90 or 270.
235      */
is90or270(int rotationDegrees)236     public static boolean is90or270(int rotationDegrees) {
237         if (rotationDegrees == 90 || rotationDegrees == 270) {
238             return true;
239         }
240         if (rotationDegrees == 0 || rotationDegrees == 180) {
241             return false;
242         }
243         throw new IllegalArgumentException("Invalid rotation degrees: " + rotationDegrees);
244     }
245 
246     /**
247      * Converts a {@link Size} to a float array of vertices.
248      */
sizeToVertices(@onNull Size size)249     public static float @NonNull [] sizeToVertices(@NonNull Size size) {
250         return new float[]{0, 0, size.getWidth(), 0, size.getWidth(), size.getHeight(), 0,
251                 size.getHeight()};
252     }
253 
254     /**
255      * Converts a {@link RectF} defined by top, left, right and bottom to an array of vertices.
256      */
rectToVertices(@onNull RectF rectF)257     public static float @NonNull [] rectToVertices(@NonNull RectF rectF) {
258         return new float[]{rectF.left, rectF.top, rectF.right, rectF.top, rectF.right, rectF.bottom,
259                 rectF.left, rectF.bottom};
260     }
261 
262     /**
263      * Checks if aspect ratio matches while tolerating rounding error.
264      *
265      * @see #isAspectRatioMatchingWithRoundingError(Size, boolean, Size, boolean)
266      */
isAspectRatioMatchingWithRoundingError( @onNull Size size1, @NonNull Size size2)267     public static boolean isAspectRatioMatchingWithRoundingError(
268             @NonNull Size size1, @NonNull Size size2) {
269         return isAspectRatioMatchingWithRoundingError(
270                 size1, /*isAccurate1=*/ false, size2, /*isAccurate2=*/ false);
271     }
272 
273     /**
274      * Checks if aspect ratio matches while tolerating rounding error.
275      *
276      * <p> One example of the usage is comparing the viewport-based crop rect from different use
277      * cases. The crop rect is rounded because pixels are integers, which may introduce an error
278      * when we check if the aspect ratio matches. For example, when
279      * {@linkplain androidx.camera.view.PreviewView}'s
280      * width/height are prime numbers 601x797, the crop rect from other use cases cannot have a
281      * matching aspect ratio even if they are based on the same viewport. This method checks the
282      * aspect ratio while tolerating a rounding error.
283      *
284      * @param size1       the rounded size1
285      * @param isAccurate1 if size1 is accurate. e.g. it's true if it's the PreviewView's
286      *                    dimension which viewport is based on
287      * @param size2       the rounded size2
288      * @param isAccurate2 if size2 is accurate.
289      */
isAspectRatioMatchingWithRoundingError( @onNull Size size1, boolean isAccurate1, @NonNull Size size2, boolean isAccurate2)290     public static boolean isAspectRatioMatchingWithRoundingError(
291             @NonNull Size size1, boolean isAccurate1, @NonNull Size size2, boolean isAccurate2) {
292         // The crop rect coordinates are rounded values. Each value is at most .5 away from their
293         // true values. So the width/height, which is the difference of 2 coordinates, are at most
294         // 1.0 away from their true value.
295         // First figure out the possible range of the aspect ratio's ture value.
296         float ratio1UpperBound;
297         float ratio1LowerBound;
298         if (isAccurate1) {
299             ratio1UpperBound = (float) size1.getWidth() / size1.getHeight();
300             ratio1LowerBound = ratio1UpperBound;
301         } else {
302             ratio1UpperBound = (size1.getWidth() + 1F) / (size1.getHeight() - 1F);
303             ratio1LowerBound = (size1.getWidth() - 1F) / (size1.getHeight() + 1F);
304         }
305         float ratio2UpperBound;
306         float ratio2LowerBound;
307         if (isAccurate2) {
308             ratio2UpperBound = (float) size2.getWidth() / size2.getHeight();
309             ratio2LowerBound = ratio2UpperBound;
310         } else {
311             ratio2UpperBound = (size2.getWidth() + 1F) / (size2.getHeight() - 1F);
312             ratio2LowerBound = (size2.getWidth() - 1F) / (size2.getHeight() + 1F);
313         }
314         // Then we check if the true value range overlaps.
315         return ratio1UpperBound >= ratio2LowerBound && ratio2UpperBound >= ratio1LowerBound;
316     }
317 
318     /**
319      * Gets the transform from one {@link RectF} to another with rotation degrees.
320      *
321      * <p> Following is how the source is mapped to the target with a 90° rotation. The rect
322      * <a, b, c, d> is mapped to <a', b', c', d'>.
323      *
324      * <pre>
325      *  a----------b               d'-----------a'
326      *  |  source  |    -90°->     |            |
327      *  d----------c               |   target   |
328      *                             |            |
329      *                             c'-----------b'
330      * </pre>
331      */
getRectToRect( @onNull RectF source, @NonNull RectF target, int rotationDegrees)332     public static @NonNull Matrix getRectToRect(
333             @NonNull RectF source, @NonNull RectF target, int rotationDegrees) {
334         return getRectToRect(source, target, rotationDegrees, /*mirroring=*/false);
335     }
336 
337     /**
338      * Gets the transform from one {@link RectF} to another with rotation degrees and mirroring.
339      *
340      * <p> Following is how the source is mapped to the target with a 90° rotation and a mirroring.
341      * The rect <a, b, c, d> is mapped to <a', b', c', d'>.
342      *
343      * <pre>
344      *  a----------b                           a'-----------d'
345      *  |  source  |    -90° + mirroring ->    |            |
346      *  d----------c                           |   target   |
347      *                                         |            |
348      *                                         b'-----------c'
349      * </pre>
350      */
getRectToRect( @onNull RectF source, @NonNull RectF target, int rotationDegrees, boolean mirroring)351     public static @NonNull Matrix getRectToRect(
352             @NonNull RectF source, @NonNull RectF target, int rotationDegrees, boolean mirroring) {
353         // Map source to normalized space.
354         Matrix matrix = new Matrix();
355         matrix.setRectToRect(source, NORMALIZED_RECT, Matrix.ScaleToFit.FILL);
356         // Add rotation.
357         matrix.postRotate(rotationDegrees);
358         if (mirroring) {
359             matrix.postScale(-1, 1);
360         }
361         // Restore the normalized space to target's coordinates.
362         matrix.postConcat(getNormalizedToBuffer(target));
363         return matrix;
364     }
365 
366     /**
367      * Gets the transform from a normalized space (-1, -1) - (1, 1) to the given rect.
368      */
getNormalizedToBuffer(@onNull Rect viewPortRect)369     public static @NonNull Matrix getNormalizedToBuffer(@NonNull Rect viewPortRect) {
370         return getNormalizedToBuffer(new RectF(viewPortRect));
371     }
372 
373     /**
374      * Updates sensor to buffer transform based on crop rect.
375      */
updateSensorToBufferTransform( @onNull Matrix original, @NonNull Rect cropRect)376     public static @NonNull Matrix updateSensorToBufferTransform(
377             @NonNull Matrix original,
378             @NonNull Rect cropRect) {
379         Matrix matrix = new Matrix(original);
380         matrix.postTranslate(-cropRect.left, -cropRect.top);
381         return matrix;
382     }
383 
384     /**
385      * Gets the transform from a normalized space (-1, -1) - (1, 1) to the given rect.
386      */
getNormalizedToBuffer(@onNull RectF viewPortRect)387     public static @NonNull Matrix getNormalizedToBuffer(@NonNull RectF viewPortRect) {
388         Matrix normalizedToBuffer = new Matrix();
389         normalizedToBuffer.setRectToRect(NORMALIZED_RECT, viewPortRect, Matrix.ScaleToFit.FILL);
390         return normalizedToBuffer;
391     }
392 
393     /**
394      * Gets the transform matrix based on exif orientation.
395      */
getExifTransform(int exifOrientation, int width, int height)396     public static @NonNull Matrix getExifTransform(int exifOrientation, int width, int height) {
397         Matrix matrix = new Matrix();
398 
399         // Map the bitmap to a normalized space and perform transform. It's more readable, and it
400         // can be tested with Robolectric's ShadowMatrix (Matrix#setPolyToPoly is currently not
401         // shadowed by ShadowMatrix).
402         RectF rect = new RectF(0, 0, width, height);
403         matrix.setRectToRect(rect, NORMALIZED_RECT, Matrix.ScaleToFit.FILL);
404 
405         // A flag that checks if the image has been rotated 90/270.
406         boolean isWidthHeightSwapped = false;
407 
408         // Transform the normalized space based on exif orientation.
409         switch (exifOrientation) {
410             case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
411                 matrix.postScale(-1f, 1f);
412                 break;
413             case ExifInterface.ORIENTATION_ROTATE_180:
414                 matrix.postRotate(180);
415                 break;
416             case ExifInterface.ORIENTATION_FLIP_VERTICAL:
417                 matrix.postScale(1f, -1f);
418                 break;
419             case ExifInterface.ORIENTATION_TRANSPOSE:
420                 // Flipped about top-left <--> bottom-right axis, it can also be represented by
421                 // flip horizontally and then rotate 270 degree clockwise.
422                 matrix.postScale(-1f, 1f);
423                 matrix.postRotate(270);
424                 isWidthHeightSwapped = true;
425                 break;
426             case ExifInterface.ORIENTATION_ROTATE_90:
427                 matrix.postRotate(90);
428                 isWidthHeightSwapped = true;
429                 break;
430             case ExifInterface.ORIENTATION_TRANSVERSE:
431                 // Flipped about top-right <--> bottom left axis, it can also be represented by
432                 // flip horizontally and then rotate 90 degree clockwise.
433                 matrix.postScale(-1f, 1f);
434                 matrix.postRotate(90);
435                 isWidthHeightSwapped = true;
436                 break;
437             case ExifInterface.ORIENTATION_ROTATE_270:
438                 matrix.postRotate(270);
439                 isWidthHeightSwapped = true;
440                 break;
441             case ExifInterface.ORIENTATION_NORMAL:
442                 // Fall-through
443             case ExifInterface.ORIENTATION_UNDEFINED:
444                 // Fall-through
445             default:
446                 break;
447         }
448 
449         // Map the normalized space back to the bitmap coordinates.
450         @SuppressWarnings("SuspiciousNameCombination")
451         RectF restoredRect = isWidthHeightSwapped ? new RectF(0, 0, height, width) : rect;
452         Matrix restore = new Matrix();
453         restore.setRectToRect(NORMALIZED_RECT, restoredRect, Matrix.ScaleToFit.FILL);
454         matrix.postConcat(restore);
455 
456         return matrix;
457     }
458 
459     /**
460      * Returns the rotation degrees of the matrix.
461      *
462      * <p>The returned degrees will be an integer between 0 and 359.
463      */
getRotationDegrees(@onNull Matrix matrix)464     public static int getRotationDegrees(@NonNull Matrix matrix) {
465         float[] values = new float[9];
466         matrix.getValues(values);
467 
468         // Calculate the degrees of rotation using the sin and cosine values from the matrix
469         float scaleX = values[Matrix.MSCALE_X];
470         float skewY = values[Matrix.MSKEW_Y];
471 
472         return within360((int) Math.round(Math.atan2(skewY, scaleX) * (180 / Math.PI)));
473     }
474 }
475