/*
* 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.transform;
import static androidx.camera.core.impl.utils.TransformUtils.isAspectRatioMatchingWithRoundingError;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.RectF;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.Logger;
import androidx.camera.core.UseCase;
import androidx.camera.core.UseCaseGroup;
import androidx.camera.core.ViewPort;
import androidx.camera.view.PreviewView;
import androidx.camera.view.TransformExperimental;
import androidx.core.util.Preconditions;
/**
* This class represents the transform from one {@link OutputTransform} to another.
*
* <p> This class can be used to map the coordinates of one {@link OutputTransform} to another,
* given that they are associated with the same {@link ViewPort}. {@link OutputTransform} can
* represent the output of a {@link UseCase} or {@link PreviewView}. For example, mapping the
* coordinates of detected objects from {@link ImageAnalysis} output to the drawing location in
* {@link PreviewView}.
*
* <pre><code>
* // imageProxy the output of an ImageAnalysis.
* OutputTransform source = ImageProxyTransformFactory().getOutputTransform(imageProxy);
* OutputTransform target = previewView.getOutputTransform();
*
* // Build the transform from ImageAnalysis to PreviewView
* CoordinateTransform coordinateTransform = new CoordinateTransform(source, target);
*
* // Detect face in ImageProxy and transform the coordinates to PreviewView.
* // The value of faceBox can be used to highlight the face in PreviewView.
* RectF faceBox = detectFaceInImageProxy(imageProxy);
* coordinateTransform.mapRect(faceBox);
*
* </code></pre>
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@TransformExperimental
public final class CoordinateTransform {
private static final String TAG = "CoordinateTransform";
private static final String MISMATCH_MSG = "The source viewport (%s) does not match the target "
+ "viewport (%s). Please make sure they are associated with the same Viewport.";
private final Matrix mMatrix;
/**
* Creates the transform between the {@code source} and the {@code target}.
*
* <p> The source and the target must be associated with the same {@link ViewPort}.
*
* @param source the source
* @see UseCaseGroup
* @see ViewPort
*/
public CoordinateTransform(@NonNull OutputTransform source,
@NonNull OutputTransform target) {
// TODO(b/137515129): This is a poor way to check if the two outputs are based on
// the same viewport. A better way is to add a matrix in use case output that represents
// the transform from sensor to surface. But it will require the view artifact to
// depend on a new internal API in the core artifact, which we can't do at the
// moment because of the version mismatch between view and core.
if (!isAspectRatioMatchingWithRoundingError(source.getViewPortSize(),
target.getViewPortSize())) {
// Mismatched aspect ratio means the outputs are not associated with the same Viewport.
Logger.w(TAG, String.format(MISMATCH_MSG, source.getViewPortSize(),
target.getViewPortSize()));
}
// Concatenate the source transform with the target transform.
mMatrix = new Matrix();
Preconditions.checkState(source.getMatrix().invert(mMatrix),
"The source transform cannot be inverted");
mMatrix.postConcat(target.getMatrix());
}
/**
* Copies the current transform to the specified {@link Matrix}.
*
* @param outMatrix a {@link android.graphics.Matrix} in which to copy the current transform.
*/
public void transform(@NonNull Matrix outMatrix) {
outMatrix.set(mMatrix);
}
/**
* Apply this transform to the array of 2D points, and write the transformed points back into
* the array
*
* @param points The array [x0, y0, x1, y1, ...] of points to transform.
* @see Matrix#mapPoints(float[])
*/
public void mapPoints(@NonNull float[] points) {
mMatrix.mapPoints(points);
}
/**
* Apply this transform to the {@link PointF}, and write the transformed points back into
* the array
*
* @param point The point to transform.
*/
public void mapPoint(@NonNull PointF point) {
float[] pointArray = new float[]{point.x, point.y};
mMatrix.mapPoints(pointArray);
point.x = pointArray[0];
point.y = pointArray[1];
}
/**
* Apply this transform to the rectangle, and write the transformed rectangle back into it.
* This is accomplished by transforming the 4 corners of rect, and then setting it to the
* bounds of those points.
*
* @param rect The rectangle to transform.
*/
public void mapRect(@NonNull RectF rect) {
mMatrix.mapRect(rect);
}
}