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