public class

ViewPorts

extends java.lang.Object

 java.lang.Object

↳androidx.camera.core.internal.ViewPorts

Gradle dependencies

compile group: 'androidx.camera', name: 'camera-core', version: '1.2.0-alpha01'

  • groupId: androidx.camera
  • artifactId: camera-core
  • version: 1.2.0-alpha01

Artifact androidx.camera:camera-core:1.2.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

Utility methods for calculating viewports.

Summary

Methods
public static java.util.Map<UseCase, Rect>calculateViewPortRects(Rect fullSensorRect, boolean isFrontCamera, Rational viewPortAspectRatio, int outputRotationDegrees, int scaleType, int layoutDirection, java.util.Map<UseCase, Size> useCaseSizes)

Calculate a set of ViewPorts based on the combination of the camera, viewport, and use cases.

public static RectFgetScaledRect(RectF fittingRect, Rational containerAspectRatio, int scaleType, boolean isFrontCamera, int layoutDirection, int rotationDegrees)

Returns the container rect that the given rect fills.

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

Methods

public static java.util.Map<UseCase, Rect> calculateViewPortRects(Rect fullSensorRect, boolean isFrontCamera, Rational viewPortAspectRatio, int outputRotationDegrees, int scaleType, int layoutDirection, java.util.Map<UseCase, Size> useCaseSizes)

Calculate a set of ViewPorts based on the combination of the camera, viewport, and use cases.

This method calculates the crop rect for each use cases. It only thinks in abstract terms like the original dimension, output rotation and desired crop rect expressed via viewport. It does not care about the use case types or the device/display rotation.

Parameters:

fullSensorRect: The full size of the viewport.
viewPortAspectRatio: The aspect ratio of the viewport.
outputRotationDegrees: Clockwise rotation to correct the surfaces to display rotation.
scaleType: The scale type to calculate
layoutDirection: The direction of layout.
useCaseSizes: The resolutions of the UseCases

Returns:

The set of Viewports that should be set for each UseCase

public static RectF getScaledRect(RectF fittingRect, Rational containerAspectRatio, int scaleType, boolean isFrontCamera, int layoutDirection, int rotationDegrees)

Returns the container rect that the given rect fills.

For FILL types, returns the largest container rect that is smaller than the view port. The returned rectangle is also required to 1) have the view port's aspect ratio and 2) be in the surface coordinates.

For FIT, returns the largest possible rect shared by all use cases.

Source

/*
 * Copyright 2020 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.core.internal;

import android.annotation.SuppressLint;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.LayoutDirection;
import android.util.Rational;
import android.util.Size;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.UseCase;
import androidx.camera.core.ViewPort;
import androidx.camera.core.internal.utils.ImageUtil;
import androidx.core.util.Preconditions;

import java.util.HashMap;
import java.util.Map;

/**
 * Utility methods for calculating viewports.
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class ViewPorts {
    private ViewPorts() {

    }

    /**
     * Calculate a set of ViewPorts based on the combination of the camera, viewport, and use cases.
     *
     * <p> This method calculates the crop rect for each use cases. It only thinks in abstract terms
     * like the original dimension, output rotation and desired crop rect expressed via viewport.
     * It does not care about the use case types or the device/display rotation.
     *
     * @param fullSensorRect        The full size of the viewport.
     * @param viewPortAspectRatio   The aspect ratio of the viewport.
     * @param outputRotationDegrees Clockwise rotation to correct the surfaces to display
     *                              rotation.
     * @param scaleType             The scale type to calculate
     * @param layoutDirection       The direction of layout.
     * @param useCaseSizes          The resolutions of the UseCases
     * @return The set of Viewports that should be set for each UseCase
     */
    @NonNull
    public static Map<UseCase, Rect> calculateViewPortRects(
            @NonNull Rect fullSensorRect,
            boolean isFrontCamera,
            @NonNull Rational viewPortAspectRatio,
            @IntRange(from = 0, to = 359) int outputRotationDegrees,
            @ViewPort.ScaleType int scaleType,
            @ViewPort.LayoutDirection int layoutDirection,
            @NonNull Map<UseCase, Size> useCaseSizes) {
        Preconditions.checkArgument(
                fullSensorRect.width() > 0 && fullSensorRect.height() > 0,
                "Cannot compute viewport crop rects zero sized sensor rect.");

        // The key to calculate the crop rect is that all the crop rect should match to the same
        // region on camera sensor. This method first calculates the shared camera region, and then
        // maps it use cases to find out their crop rects.

        // Calculate the mapping between sensor buffer and UseCases, and the sensor rect shared
        // by all use cases.
        RectF fullSensorRectF = new RectF(fullSensorRect);
        Map<UseCase, Matrix> useCaseToSensorTransformations = new HashMap<>();
        RectF sensorIntersectionRect = new RectF(fullSensorRect);
        for (Map.Entry<UseCase, Size> entry : useCaseSizes.entrySet()) {
            // Calculate the transformation from UseCase to sensor.
            Matrix useCaseToSensorTransformation = new Matrix();
            RectF srcRect = new RectF(0, 0, entry.getValue().getWidth(),
                    entry.getValue().getHeight());
            useCaseToSensorTransformation.setRectToRect(srcRect, fullSensorRectF,
                    Matrix.ScaleToFit.CENTER);
            useCaseToSensorTransformations.put(entry.getKey(), useCaseToSensorTransformation);

            // Calculate the UseCase intersection in sensor coordinates.
            RectF useCaseSensorRect = new RectF();
            useCaseToSensorTransformation.mapRect(useCaseSensorRect, srcRect);
            sensorIntersectionRect.intersect(useCaseSensorRect);
        }

        // Crop the shared sensor rect based on viewport parameters.
        Rational rotatedViewPortAspectRatio = ImageUtil.getRotatedAspectRatio(
                outputRotationDegrees, viewPortAspectRatio);
        RectF viewPortRect = getScaledRect(
                sensorIntersectionRect, rotatedViewPortAspectRatio, scaleType, isFrontCamera,
                layoutDirection, outputRotationDegrees);

        // Map the cropped shared sensor rect to UseCase coordinates.
        Map<UseCase, Rect> useCaseOutputRects = new HashMap<>();
        RectF useCaseOutputRect = new RectF();
        Matrix sensorToUseCaseTransformation = new Matrix();
        for (Map.Entry<UseCase, Matrix> entry : useCaseToSensorTransformations.entrySet()) {
            // Transform the sensor crop rect to UseCase coordinates.
            entry.getValue().invert(sensorToUseCaseTransformation);
            sensorToUseCaseTransformation.mapRect(useCaseOutputRect, viewPortRect);
            Rect outputCropRect = new Rect();
            useCaseOutputRect.round(outputCropRect);
            useCaseOutputRects.put(entry.getKey(), outputCropRect);
        }
        return useCaseOutputRects;
    }

    /**
     * Returns the container rect that the given rect fills.
     *
     * <p> For FILL types, returns the largest container rect that is smaller than the view port.
     * The returned rectangle is also required to 1) have the view port's aspect ratio and 2) be
     * in the surface coordinates.
     *
     * <p> For FIT, returns the largest possible rect shared by all use cases.
     */
    @SuppressLint("SwitchIntDef")
    @NonNull
    public static RectF getScaledRect(
            @NonNull RectF fittingRect,
            @NonNull Rational containerAspectRatio,
            @ViewPort.ScaleType int scaleType,
            boolean isFrontCamera,
            @ViewPort.LayoutDirection int layoutDirection,
            @IntRange(from = 0, to = 359) int rotationDegrees) {
        if (scaleType == ViewPort.FIT) {
            // Return the fitting rect if the rect is fully covered by the container.
            return fittingRect;
        }
        // Using Matrix' convenience methods fill the rect into the containing rect with given
        // aspect ratio.
        // NOTE: By using the Matrix#setRectToRect, we assume the "start" is always (0, 0) and
        // the "end" is always (w, h), which is NOT always true depending on rotation, layout
        // orientation and/or camera lens facing. We need to correct the rect based on rotation and
        // layout direction.
        Matrix viewPortToSurfaceTransformation = new Matrix();
        RectF viewPortRect = new RectF(0, 0, containerAspectRatio.getNumerator(),
                containerAspectRatio.getDenominator());
        switch (scaleType) {
            case ViewPort.FILL_CENTER:
                viewPortToSurfaceTransformation.setRectToRect(
                        viewPortRect, fittingRect, Matrix.ScaleToFit.CENTER);
                break;
            case ViewPort.FILL_START:
                viewPortToSurfaceTransformation.setRectToRect(
                        viewPortRect, fittingRect, Matrix.ScaleToFit.START);
                break;
            case ViewPort.FILL_END:
                viewPortToSurfaceTransformation.setRectToRect(
                        viewPortRect, fittingRect, Matrix.ScaleToFit.END);
                break;
            default:
                throw new IllegalStateException("Unexpected scale type: " + scaleType);
        }

        RectF viewPortRectInSurfaceCoordinates = new RectF();
        viewPortToSurfaceTransformation.mapRect(viewPortRectInSurfaceCoordinates, viewPortRect);

        // Correct the crop rect based on rotation and layout direction.
        return correctStartOrEnd(
                shouldMirrorStartAndEnd(isFrontCamera, layoutDirection),
                rotationDegrees,
                fittingRect,
                viewPortRectInSurfaceCoordinates);
    }

    /**
     * Correct viewport based on rotation and layout direction.
     *
     * <p> Both rotation and mirroring change the definition of the "start" and "end" in
     * scale type. For rotation, since the value is clockwise rotation should be applied to the
     * output buffer, the start/end point should be rotated counterclockwisely. If mirroring is
     * needed, the start/end point should be mirrored based on the upright direction of the
     * image.
     */
    private static RectF correctStartOrEnd(boolean isMirrored,
            @IntRange(from = 0, to = 359) int rotationDegrees,
            RectF containerRect,
            RectF cropRect) {
        // For each scenario there is an illustration of the output buffer without correction.
        // The arrow represents the opposite direction of gravity. The start/end point should
        // rotate counterclockwisely based on rotationDegrees, and mirror along the line of the
        // arrow if mirroring is needed.

        //
        // Start +-----+
        //       |  ^  |
        //       +-----+  End
        //
        boolean ltrRotation0 = rotationDegrees == 0 && !isMirrored;
        //
        // Start +-----+     90°     +-----+ End  Mirrored  Start +-----+
        //       |  ^  |    ===>     |  <  |        ==>           |  <  |
        //       +-----+ End   Start +-----+                      +-----+ End
        //
        boolean rtlRotation90 = rotationDegrees == 90 && isMirrored;
        if (ltrRotation0 || rtlRotation90) {
            return cropRect;
        }

        //
        // Start +-----+ Mirrored  +-----+ Start
        //       |  ^  |   ===>    |  ^  |
        //       +-----+ End   End +-----+
        //
        boolean rtlRotation0 = rotationDegrees == 0 && isMirrored;
        //
        // Start +-----+   270°   +-----+ Start
        //       |  ^  |   ===>   |  >  |
        //       +-----+ End  End +-----+
        //
        boolean ltrRotation270 = rotationDegrees == 270 && !isMirrored;
        if (rtlRotation0 || ltrRotation270) {
            return flipHorizontally(cropRect, containerRect.centerX());
        }

        //
        // Start +-----+    90°     +-----+ End
        //       |  ^  |   ===>     |  <  |
        //       +-----+ End  Start +-----+
        //
        boolean ltrRotation90 = rotationDegrees == 90 && !isMirrored;
        //
        // Start +-----+   180°  End +-----+   Mirrored    +-----+ End
        //       |  ^  |   ===>      |  v  |     ==>       |  v  |
        //       +-----+ End         +-----+ Start   Start +-----+
        //
        boolean rtlRotation180 = rotationDegrees == 180 && isMirrored;
        if (ltrRotation90 || rtlRotation180) {
            return flipVertically(cropRect, containerRect.centerY());
        }

        //
        // Start +-----+   180°  End +-----+
        //       |  ^  |   ===>      |  v  |
        //       +-----+ End         +-----+ Start
        //
        boolean ltrRotation180 = rotationDegrees == 180 && !isMirrored;
        //
        // Start +-----+   270°   +-----+ Start  Mirrored  End +-----+
        //       |  ^  |   ===>   |  >  |           ==>        |  >  |
        //       +-----+ End  End +-----+                      +-----+ Start
        //
        boolean rtlRotation270 = rotationDegrees == 270 && isMirrored;
        if (ltrRotation180 || rtlRotation270) {
            return flipHorizontally(flipVertically(cropRect, containerRect.centerY()),
                    containerRect.centerX());
        }

        throw new IllegalArgumentException("Invalid argument: mirrored " + isMirrored + " "
                + "rotation " + rotationDegrees);
    }

    /**
     * Checks if the start/end direction in scale type should be mirrored.
     *
     * <p> They should be mirrored if one and only one of the following is true: the front camera is
     * used or layout direction is RTL.
     */
    private static boolean shouldMirrorStartAndEnd(boolean isFrontCamera,
            @ViewPort.LayoutDirection int layoutDirection) {
        return isFrontCamera ^ layoutDirection == LayoutDirection.RTL;
    }

    private static RectF flipHorizontally(RectF original, float flipLineX) {
        return new RectF(
                flipX(original.right, flipLineX),
                original.top,
                flipX(original.left, flipLineX),
                original.bottom);
    }

    private static RectF flipVertically(RectF original, float flipLineY) {
        return new RectF(
                original.left,
                flipY(original.bottom, flipLineY),
                original.right,
                flipY(original.top, flipLineY));
    }

    private static float flipX(float x, float flipLineX) {
        return flipLineX + flipLineX - x;
    }

    private static float flipY(float y, float flipLineY) {
        return flipLineY + flipLineY - y;
    }
}