/*
* 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.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.
*/
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;
}
}