public class

TransformUtils

extends java.lang.Object

 java.lang.Object

↳androidx.camera.view.TransformUtils

Overview

Utility class for transform.

The vertices representation uses a float array to represent a rectangle with arbitrary rotation and rotation-direction. It could be otherwise represented by a triple of a , a rotation degrees integer and a boolean flag for the rotation-direction (clockwise v.s. counter-clockwise). TODO(b/179827713): merge this with ImageUtil.

Summary

Fields
public static final RectFNORMALIZED_RECT

Methods
public static MatrixgetExifTransform(int exifOrientation, int width, int height)

Gets the transform matrix based on exif orientation.

public static MatrixgetNormalizedToBuffer(Rect viewPortRect)

Gets the transform from a normalized space (-1, -1) - (1, 1) to the given rect.

public static MatrixgetRectToRect(RectF source, RectF target, int rotationDegrees)

Gets the transform from one to another with rotation degrees.

public static booleanis90or270(int rotationDegrees)

Returns true if the rotation degrees is 90 or 270.

public static booleanisAspectRatioMatchingWithRoundingError(Size size1, boolean isAccurate1, Size size2, boolean isAccurate2)

Checks if aspect ratio matches while tolerating rounding error.

public static floatmax(float value1, float value2, float value3, float value4)

Returns the max value.

public static floatmin(float value1, float value2, float value3, float value4)

Returns the min value.

public static SizerectToSize(Rect rect)

Gets the size of the .

public static float[]rectToVertices(RectF rectF)

Converts a defined by top, left, right and bottom to an array of vertices.

public static float[]sizeToVertices(Size size)

Converts a to a float array of vertices.

public static intsurfaceRotationToRotationDegrees(int rotationValue)

Converts rotation to rotation degrees: 90, 180, 270 or 0.

public static RectFverticesToRect(float[] vertices[])

Converts an array of vertices to a .

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final RectF NORMALIZED_RECT

Methods

public static Size rectToSize(Rect rect)

Gets the size of the .

public static RectF verticesToRect(float[] vertices[])

Converts an array of vertices to a .

public static float max(float value1, float value2, float value3, float value4)

Returns the max value.

public static float min(float value1, float value2, float value3, float value4)

Returns the min value.

public static int surfaceRotationToRotationDegrees(int rotationValue)

Converts rotation to rotation degrees: 90, 180, 270 or 0.

public static boolean is90or270(int rotationDegrees)

Returns true if the rotation degrees is 90 or 270.

public static float[] sizeToVertices(Size size)

Converts a to a float array of vertices.

public static float[] rectToVertices(RectF rectF)

Converts a defined by top, left, right and bottom to an array of vertices.

public static boolean isAspectRatioMatchingWithRoundingError(Size size1, boolean isAccurate1, Size size2, boolean isAccurate2)

Checks if aspect ratio matches while tolerating rounding error.

One example of the usage is comparing the viewport-based crop rect from different use cases. The crop rect is rounded because pixels are integers, which may introduce an error when we check if the aspect ratio matches. For example, when PreviewView's width/height are prime numbers 601x797, the crop rect from other use cases cannot have a matching aspect ratio even if they are based on the same viewport. This method checks the aspect ratio while tolerating a rounding error.

Parameters:

size1: the rounded size1
isAccurate1: if size1 is accurate. e.g. it's true if it's the PreviewView's dimension which viewport is based on
size2: the rounded size2
isAccurate2: if size2 is accurate.

public static Matrix getRectToRect(RectF source, RectF target, int rotationDegrees)

Gets the transform from one to another with rotation degrees.

Following is how the source is mapped to the target with a 90° rotation. The rect is mapped to .

  a----------b               d'-----------a'
  |  source  |    -90°->     |            |
  d----------c               |   target   |
                             |            |
                             c'-----------b'
 

public static Matrix getNormalizedToBuffer(Rect viewPortRect)

Gets the transform from a normalized space (-1, -1) - (1, 1) to the given rect.

public static Matrix getExifTransform(int exifOrientation, int width, int height)

Gets the transform matrix based on exif orientation.

Source

/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.camera.view;

import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.media.ExifInterface;
import android.util.Size;
import android.view.Surface;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;

/**
 * Utility class for transform.
 *
 * <p> The vertices representation uses a float array to represent a rectangle with arbitrary
 * rotation and rotation-direction. It could be otherwise represented by a triple of a
 * {@link RectF}, a rotation degrees integer and a boolean flag for the rotation-direction
 * (clockwise v.s. counter-clockwise).
 *
 * TODO(b/179827713): merge this with {@link androidx.camera.core.internal.utils.ImageUtil}.
 *
 * @hide
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class TransformUtils {

    // Normalized space (-1, -1) - (1, 1).
    public static final RectF NORMALIZED_RECT = new RectF(-1, -1, 1, 1);

    private TransformUtils() {
    }

    /**
     * Gets the size of the {@link Rect}.
     */
    @NonNull
    public static Size rectToSize(@NonNull Rect rect) {
        return new Size(rect.width(), rect.height());
    }

    /**
     * Converts an array of vertices to a {@link RectF}.
     */
    @NonNull
    public static RectF verticesToRect(@NonNull float[] vertices) {
        return new RectF(
                min(vertices[0], vertices[2], vertices[4], vertices[6]),
                min(vertices[1], vertices[3], vertices[5], vertices[7]),
                max(vertices[0], vertices[2], vertices[4], vertices[6]),
                max(vertices[1], vertices[3], vertices[5], vertices[7])
        );
    }

    /**
     * Returns the max value.
     */
    public static float max(float value1, float value2, float value3, float value4) {
        return Math.max(Math.max(value1, value2), Math.max(value3, value4));
    }

    /**
     * Returns the min value.
     */
    public static float min(float value1, float value2, float value3, float value4) {
        return Math.min(Math.min(value1, value2), Math.min(value3, value4));
    }

    /**
     * Converts {@link Surface} rotation to rotation degrees: 90, 180, 270 or 0.
     */
    public static int surfaceRotationToRotationDegrees(int rotationValue) {
        switch (rotationValue) {
            case Surface.ROTATION_0:
                return 0;
            case Surface.ROTATION_90:
                return 90;
            case Surface.ROTATION_180:
                return 180;
            case Surface.ROTATION_270:
                return 270;
            default:
                throw new IllegalStateException("Unexpected rotation value " + rotationValue);
        }
    }

    /**
     * Returns true if the rotation degrees is 90 or 270.
     */
    public static boolean is90or270(int rotationDegrees) {
        if (rotationDegrees == 90 || rotationDegrees == 270) {
            return true;
        }
        if (rotationDegrees == 0 || rotationDegrees == 180) {
            return false;
        }
        throw new IllegalArgumentException("Invalid rotation degrees: " + rotationDegrees);
    }

    /**
     * Converts a {@link Size} to a float array of vertices.
     */
    @NonNull
    public static float[] sizeToVertices(@NonNull Size size) {
        return new float[]{0, 0, size.getWidth(), 0, size.getWidth(), size.getHeight(), 0,
                size.getHeight()};
    }

    /**
     * Converts a {@link RectF} defined by top, left, right and bottom to an array of vertices.
     */
    @NonNull
    public static float[] rectToVertices(@NonNull RectF rectF) {
        return new float[]{rectF.left, rectF.top, rectF.right, rectF.top, rectF.right, rectF.bottom,
                rectF.left, rectF.bottom};
    }

    /**
     * Checks if aspect ratio matches while tolerating rounding error.
     *
     * <p> One example of the usage is comparing the viewport-based crop rect from different use
     * cases. The crop rect is rounded because pixels are integers, which may introduce an error
     * when we check if the aspect ratio matches. For example, when {@link PreviewView}'s
     * width/height are prime numbers 601x797, the crop rect from other use cases cannot have a
     * matching aspect ratio even if they are based on the same viewport. This method checks the
     * aspect ratio while tolerating a rounding error.
     *
     * @param size1       the rounded size1
     * @param isAccurate1 if size1 is accurate. e.g. it's true if it's the PreviewView's
     *                    dimension which viewport is based on
     * @param size2       the rounded size2
     * @param isAccurate2 if size2 is accurate.
     */
    public static boolean isAspectRatioMatchingWithRoundingError(
            @NonNull Size size1, boolean isAccurate1, @NonNull Size size2, boolean isAccurate2) {
        // The crop rect coordinates are rounded values. Each value is at most .5 away from their
        // true values. So the width/height, which is the difference of 2 coordinates, are at most
        // 1.0 away from their true value.
        // First figure out the possible range of the aspect ratio's ture value.
        float ratio1UpperBound;
        float ratio1LowerBound;
        if (isAccurate1) {
            ratio1UpperBound = (float) size1.getWidth() / size1.getHeight();
            ratio1LowerBound = ratio1UpperBound;
        } else {
            ratio1UpperBound = (size1.getWidth() + 1F) / (size1.getHeight() - 1F);
            ratio1LowerBound = (size1.getWidth() - 1F) / (size1.getHeight() + 1F);
        }
        float ratio2UpperBound;
        float ratio2LowerBound;
        if (isAccurate2) {
            ratio2UpperBound = (float) size2.getWidth() / size2.getHeight();
            ratio2LowerBound = ratio2UpperBound;
        } else {
            ratio2UpperBound = (size2.getWidth() + 1F) / (size2.getHeight() - 1F);
            ratio2LowerBound = (size2.getWidth() - 1F) / (size2.getHeight() + 1F);
        }
        // Then we check if the true value range overlaps.
        return ratio1UpperBound >= ratio2LowerBound && ratio2UpperBound >= ratio1LowerBound;
    }

    /**
     * Gets the transform from one {@link Rect} to another with rotation degrees.
     *
     * <p> Following is how the source is mapped to the target with a 90° rotation. The rect
     * <a, b, c, d> is mapped to <a', b', c', d'>.
     *
     * <pre>
     *  a----------b               d'-----------a'
     *  |  source  |    -90°->     |            |
     *  d----------c               |   target   |
     *                             |            |
     *                             c'-----------b'
     * </pre>
     */
    @NonNull
    public static Matrix getRectToRect(
            @NonNull RectF source, @NonNull RectF target, int rotationDegrees) {
        // Map source to normalized space.
        Matrix matrix = new Matrix();
        matrix.setRectToRect(source, NORMALIZED_RECT, Matrix.ScaleToFit.FILL);
        // Add rotation.
        matrix.postRotate(rotationDegrees);
        // Restore the normalized space to target's coordinates.
        matrix.postConcat(getNormalizedToBuffer(target));
        return matrix;
    }

    /**
     * Gets the transform from a normalized space (-1, -1) - (1, 1) to the given rect.
     */
    @NonNull
    public static Matrix getNormalizedToBuffer(@NonNull Rect viewPortRect) {
        return getNormalizedToBuffer(new RectF(viewPortRect));
    }

    /**
     * Gets the transform from a normalized space (-1, -1) - (1, 1) to the given rect.
     */
    @NonNull
    private static Matrix getNormalizedToBuffer(@NonNull RectF viewPortRect) {
        Matrix normalizedToBuffer = new Matrix();
        normalizedToBuffer.setRectToRect(NORMALIZED_RECT, viewPortRect, Matrix.ScaleToFit.FILL);
        return normalizedToBuffer;
    }

    /**
     * Gets the transform matrix based on exif orientation.
     */
    @NonNull
    public static Matrix getExifTransform(int exifOrientation, int width, int height) {
        Matrix matrix = new Matrix();

        // Map the bitmap to a normalized space and perform transform. It's more readable, and it
        // can be tested with Robolectric's ShadowMatrix (Matrix#setPolyToPoly is currently not
        // shadowed by ShadowMatrix).
        RectF rect = new RectF(0, 0, width, height);
        matrix.setRectToRect(rect, NORMALIZED_RECT, Matrix.ScaleToFit.FILL);

        // A flag that checks if the image has been rotated 90/270.
        boolean isWidthHeightSwapped = false;

        // Transform the normalized space based on exif orientation.
        switch (exifOrientation) {
            case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
                matrix.postScale(-1f, 1f);
                break;
            case ExifInterface.ORIENTATION_ROTATE_180:
                matrix.postRotate(180);
                break;
            case ExifInterface.ORIENTATION_FLIP_VERTICAL:
                matrix.postScale(1f, -1f);
                break;
            case ExifInterface.ORIENTATION_TRANSPOSE:
                // Flipped about top-left <--> bottom-right axis, it can also be represented by
                // flip horizontally and then rotate 270 degree clockwise.
                matrix.postScale(-1f, 1f);
                matrix.postRotate(270);
                isWidthHeightSwapped = true;
                break;
            case ExifInterface.ORIENTATION_ROTATE_90:
                matrix.postRotate(90);
                isWidthHeightSwapped = true;
                break;
            case ExifInterface.ORIENTATION_TRANSVERSE:
                // Flipped about top-right <--> bottom left axis, it can also be represented by
                // flip horizontally and then rotate 90 degree clockwise.
                matrix.postScale(-1f, 1f);
                matrix.postRotate(90);
                isWidthHeightSwapped = true;
                break;
            case ExifInterface.ORIENTATION_ROTATE_270:
                matrix.postRotate(270);
                isWidthHeightSwapped = true;
                break;
            case ExifInterface.ORIENTATION_NORMAL:
                // Fall-through
            case ExifInterface.ORIENTATION_UNDEFINED:
                // Fall-through
            default:
                break;
        }

        // Map the normalized space back to the bitmap coordinates.
        RectF restoredRect = isWidthHeightSwapped ? new RectF(0, 0, height, width) : rect;
        Matrix restore = new Matrix();
        restore.setRectToRect(NORMALIZED_RECT, restoredRect, Matrix.ScaleToFit.FILL);
        matrix.postConcat(restore);

        return matrix;
    }
}