Switch MLKitAnalyzer to use the new sensorToBuffer API

Instead of relying on the deprecated CoordinateTransform, we pass the sensorToBuffer matrix to MLKitAnalyzer

Bug: 285043389
Test: manual test and ./gradlew bOS
Change-Id: I419e6016da251ec6d2c42068ee14dbc5d2418b1c
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
index 4454f71..2e41dc8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
@@ -141,6 +141,22 @@
     }
 
     /**
+     * Rotates {@link SizeF} according to the rotation degrees.
+     *
+     * <p> A 640, 480 rect rotated 90 degrees clockwise will become a 480, 640 rect.
+     */
+    @NonNull
+    public static RectF rotateRect(@NonNull RectF rect, int rotationDegrees) {
+        Preconditions.checkArgument(rotationDegrees % 90 == 0,
+                "Invalid rotation degrees: " + rotationDegrees);
+        if (is90or270(within360(rotationDegrees))) {
+            return new RectF(0, 0, /*right=*/rect.height(),  /*bottom=*/rect.width());
+        } else {
+            return rect;
+        }
+    }
+
+    /**
      * Gets the size after cropping and rotating.
      *
      * @return rotated size
diff --git a/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/MlKitAnalyzer.java b/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/MlKitAnalyzer.java
index 55b0cfd..605c78a 100644
--- a/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/MlKitAnalyzer.java
+++ b/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/MlKitAnalyzer.java
@@ -16,6 +16,8 @@
 package androidx.camera.mlkit.vision;
 
 import static androidx.camera.core.ImageAnalysis.COORDINATE_SYSTEM_ORIGINAL;
+import static androidx.camera.core.impl.utils.TransformUtils.getRectToRect;
+import static androidx.camera.core.impl.utils.TransformUtils.rotateRect;
 
 import static com.google.android.gms.common.internal.Preconditions.checkArgument;
 import static com.google.mlkit.vision.interfaces.Detector.TYPE_BARCODE_SCANNING;
@@ -23,6 +25,7 @@
 import static com.google.mlkit.vision.interfaces.Detector.TYPE_TEXT_RECOGNITION;
 
 import android.graphics.Matrix;
+import android.graphics.RectF;
 import android.media.Image;
 import android.util.Size;
 
@@ -35,9 +38,7 @@
 import androidx.camera.core.ImageProxy;
 import androidx.camera.core.Logger;
 import androidx.camera.view.TransformExperimental;
-import androidx.camera.view.transform.CoordinateTransform;
 import androidx.camera.view.transform.ImageProxyTransformFactory;
-import androidx.camera.view.transform.OutputTransform;
 import androidx.core.util.Consumer;
 
 import com.google.android.gms.tasks.Task;
@@ -61,8 +62,8 @@
  * <p> This class handles the coordinate transformation between ML Kit output and the target
  * coordinate system. Using the {@code targetCoordinateSystem} set in the constructor, it
  * calculates the {@link Matrix} with the value provided by CameraX via
- * {@link ImageAnalysis.Analyzer#updateTransform} and forwards it to the ML Kit {@code Detector}. The
- * coordinates returned by MLKit will be in the specified coordinate system.
+ * {@link ImageAnalysis.Analyzer#updateTransform} and forwards it to the ML Kit {@code Detector}.
+ * The coordinates returned by MLKit will be in the specified coordinate system.
  *
  * <p> This class is designed to work seamlessly with the {@code CameraController} class in
  * camera-view. When used with {@link ImageAnalysis} in camera-core, the following scenarios may
@@ -155,7 +156,7 @@
     @OptIn(markerClass = TransformExperimental.class)
     public final void analyze(@NonNull ImageProxy imageProxy) {
         // By default, the matrix is identity for COORDINATE_SYSTEM_ORIGINAL.
-        Matrix transform = new Matrix();
+        Matrix analysisToTarget = new Matrix();
         if (mTargetCoordinateSystem != COORDINATE_SYSTEM_ORIGINAL) {
             // Calculate the transform if not COORDINATE_SYSTEM_ORIGINAL.
             Matrix sensorToTarget = mSensorToTarget;
@@ -166,16 +167,24 @@
                 imageProxy.close();
                 return;
             }
-            OutputTransform analysisTransform =
-                    mImageAnalysisTransformFactory.getOutputTransform(imageProxy);
-            Size cropRectSize = new Size(imageProxy.getCropRect().width(),
-                    imageProxy.getCropRect().height());
-            CoordinateTransform coordinateTransform = new CoordinateTransform(analysisTransform,
-                    new OutputTransform(sensorToTarget, cropRectSize));
-            coordinateTransform.transform(transform);
+            Matrix sensorToAnalysis =
+                    new Matrix(imageProxy.getImageInfo().getSensorToBufferTransformMatrix());
+            // Calculate the rotation added by ML Kit.
+            RectF sourceRect = new RectF(0, 0, imageProxy.getWidth(),
+                    imageProxy.getHeight());
+            RectF bufferRect = rotateRect(sourceRect,
+                    imageProxy.getImageInfo().getRotationDegrees());
+            Matrix analysisToMlKitRotation = getRectToRect(sourceRect, bufferRect,
+                    imageProxy.getImageInfo().getRotationDegrees());
+            // Concat the MLKit transformation with sensor to Analysis.
+            sensorToAnalysis.postConcat(analysisToMlKitRotation);
+            // Invert to get analysis to sensor.
+            sensorToAnalysis.invert(analysisToTarget);
+            // Concat sensor to target to get analysisToTarget.
+            analysisToTarget.postConcat(sensorToTarget);
         }
         // Detect the image recursively, starting from index 0.
-        detectRecursively(imageProxy, 0, transform, new HashMap<>(), new HashMap<>());
+        detectRecursively(imageProxy, 0, analysisToTarget, new HashMap<>(), new HashMap<>());
     }
 
     /**
diff --git a/camera/camera-mlkit-vision/src/test/java/androidx/camera/mlkit/vision/MlKitAnalyzerTest.kt b/camera/camera-mlkit-vision/src/test/java/androidx/camera/mlkit/vision/MlKitAnalyzerTest.kt
index c4dcc0f..73919a9 100644
--- a/camera/camera-mlkit-vision/src/test/java/androidx/camera/mlkit/vision/MlKitAnalyzerTest.kt
+++ b/camera/camera-mlkit-vision/src/test/java/androidx/camera/mlkit/vision/MlKitAnalyzerTest.kt
@@ -16,13 +16,14 @@
 
 package androidx.camera.mlkit.vision
 
-import android.graphics.Matrix
 import android.graphics.Rect
+import android.graphics.RectF
 import android.media.Image
 import android.os.Build
 import android.util.Size
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageProxy
+import androidx.camera.core.impl.utils.TransformUtils.getRectToRect
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor
 import androidx.camera.testing.fakes.FakeImageInfo
 import androidx.camera.testing.fakes.FakeImageProxy
@@ -51,7 +52,14 @@
         private const val RETURN_VALUE = "return value"
         private const val TIMESTAMP = 100L
         private const val ROTATION_DEGREES = 180
-        private val CROP_RECT = Rect(0, 0, 640, 480)
+        private val SENSOR_RECT = Rect(0, 0, 4000, 3000)
+        private val VIEW_RECT = Rect(0, 0, 1024, 768)
+        private val IMAGE_ANALYSIS_RECT = Rect(0, 0, 640, 480)
+        private val SENSOR_TO_BUFFER = getRectToRect(
+            RectF(SENSOR_RECT),
+            RectF(IMAGE_ANALYSIS_RECT),
+            0
+        )
     }
 
     @Test
@@ -150,7 +158,7 @@
     @Test
     fun transformationAndRotationIsCorrect() {
         // Arrange.
-        val additionalTransform = Matrix()
+        val additionalTransform = getRectToRect(RectF(SENSOR_RECT), RectF(VIEW_RECT), 0)
         additionalTransform.setScale(2F, 2F)
         val detector = FakeDetector(RETURN_VALUE, TYPE_BARCODE_SCANNING)
         val analyzer = MlKitAnalyzer(
@@ -165,25 +173,23 @@
         analyzer.analyze(createFakeImageProxy())
 
         // Assert that the matrix is passed to the MLKit detector.
-        val expected = floatArrayOf(-0.00625F, 0F, 2.0F, 0F, -0.0083333F, 2.0F, 0.0F, 0.0F, 1.0F)
+        val expected = floatArrayOf(-12.5F, 0F, 8000F, 0F, -12.5F, 6000F, 0F, 0F, 1F)
         val actual = FloatArray(9)
         detector.latestMatrix!!.getValues(actual)
-        actual.forEachIndexed { i, element ->
-            // Assert allowing float rounding error.
-            assertThat(expected[i]).isWithin(1E-4F).of(element)
-        }
+        assertThat(actual).usingTolerance(1E-3).containsExactly(expected).inOrder()
     }
 
     private fun createFakeImageProxy(): ImageProxy {
         val imageInfo = FakeImageInfo()
         imageInfo.timestamp = TIMESTAMP
         imageInfo.rotationDegrees = ROTATION_DEGREES
+        imageInfo.sensorToBufferTransformMatrix = SENSOR_TO_BUFFER
 
         val imageProxy = FakeImageProxy(imageInfo)
         imageProxy.image = mock(Image::class.java)
-        imageProxy.width = CROP_RECT.width()
-        imageProxy.height = CROP_RECT.height()
-        imageProxy.setCropRect(CROP_RECT)
+        imageProxy.width = IMAGE_ANALYSIS_RECT.width()
+        imageProxy.height = IMAGE_ANALYSIS_RECT.height()
+        imageProxy.setCropRect(IMAGE_ANALYSIS_RECT)
 
         return imageProxy
     }
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index 9c2f968..d935dd4 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -80,7 +80,6 @@
 import androidx.camera.video.Recording;
 import androidx.camera.video.VideoCapture;
 import androidx.camera.video.VideoRecordEvent;
-import androidx.camera.view.transform.OutputTransform;
 import androidx.camera.view.video.AudioConfig;
 import androidx.core.content.PermissionChecker;
 import androidx.core.util.Consumer;
@@ -1098,16 +1097,16 @@
 
     @OptIn(markerClass = {TransformExperimental.class})
     @MainThread
-    void updatePreviewViewTransform(@Nullable OutputTransform outputTransform) {
+    void updatePreviewViewTransform(@Nullable Matrix matrix) {
         checkMainThread();
         if (mAnalysisAnalyzer == null) {
             return;
         }
-        if (outputTransform == null) {
+        if (matrix == null) {
             mAnalysisAnalyzer.updateTransform(null);
         } else if (mAnalysisAnalyzer.getTargetCoordinateSystem()
                 == COORDINATE_SYSTEM_VIEW_REFERENCED) {
-            mAnalysisAnalyzer.updateTransform(outputTransform.getMatrix());
+            mAnalysisAnalyzer.updateTransform(matrix);
         }
     }
 
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
index f2a95b5..cb853a1 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
@@ -660,7 +660,7 @@
         mPreviewViewMeteringPointFactory.recalculate(new Size(getWidth(), getHeight()),
                 getLayoutDirection());
         if (mCameraController != null) {
-            mCameraController.updatePreviewViewTransform(getOutputTransform());
+            mCameraController.updatePreviewViewTransform(getSensorToViewTransform());
         }
     }
 
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
index 22a51d3..5ac8b85 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
@@ -44,7 +44,6 @@
 import androidx.camera.video.Quality
 import androidx.camera.video.QualitySelector
 import androidx.camera.view.CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED
-import androidx.camera.view.transform.OutputTransform
 import androidx.concurrent.futures.CallbackToFutureAdapter
 import androidx.test.annotation.UiThreadTest
 import androidx.test.core.app.ApplicationProvider
@@ -277,10 +276,7 @@
             }
         }
         controller.setImageAnalysisAnalyzer(mainThreadExecutor(), analyzer)
-        val outputTransform = previewTransform?.let {
-            OutputTransform(it, Size(1, 1))
-        }
-        controller.updatePreviewViewTransform(outputTransform)
+        controller.updatePreviewViewTransform(previewTransform)
         return matrix
     }